feat(v1): REQ-02 CLI, tutorial Ping e remoção da GUI legada
Adiciona PluginStoreCli para smoke de instalação de JAR; reescreve o tutorial de ping e limitações didáticas; remove MainWindow/LegacyPluginWorkspace e move Sobre para AboutDialog na shell. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
ef33245f49
commit
fc9739a998
17 files changed
+242
-714
No files matched your search
@@ -51,6 +51,8 @@ MeusPlugins.plugins.NomeDaClasse
|
||||
Para serem carregados, os plugins devem estar empacotados como arquivos JAR
|
||||
e colocados no diretório plugins da aplicação.
|
||||
|
||||
O catálogo de build (`plugins.registry.json`) cobre só **chat** e **screencast** no CI. Exercícios (ex. [ping](17_tutorial_ping_aula.md)) ficam fora do repositório — o aluno instala o JAR localmente após `ant jar`.
|
||||
|
||||
### Carregamento de plugins
|
||||
|
||||
O `PluginManager.loadPlugins()` (chamado pela `MainShellFrame`):
|
||||
|
||||
@@ -28,4 +28,16 @@ Dois módulos buildáveis no catálogo; o chat unifica o antigo mpChat (multicas
|
||||
|
||||
## Exercício Ping
|
||||
|
||||
Material em [17_tutorial_ping_aula.md](17_tutorial_ping_aula.md) e pasta `plugins/ping/` (sem JAR no repositório).
|
||||
Material em [17_tutorial_ping_aula.md](17_tutorial_ping_aula.md) e pasta `plugins/ping/` (sem código/JAR no repositório). Não entra no `plugins.registry.json` nem no gate CI.
|
||||
|
||||
## Limitações didáticas (não são bugs)
|
||||
|
||||
Os plugins de referência são **exemplos de sala de aula**, não produtos completos:
|
||||
|
||||
| Plugin | Inclui | Não inclui (de propósito) |
|
||||
| --- | --- | --- |
|
||||
| **chat** | texto, sala geral, unicast/multicast/TCP, TCP privado básico | imagens, moderação, histórico persistente, criptografia |
|
||||
| **screencast** | stream read-only, avisos de sessão | controle remoto do mouse/teclado, áudio |
|
||||
| **ping** (exercício do aluno) | RTT, ping uma vez/contínuo, parar e avisar o peer | ICMP do SO, gráficos, SLA |
|
||||
|
||||
Para evoluir: copie o padrão (`Plugin` + mensagens + painel na aba) e acrescente features no **seu** JAR — sem inflar o núcleo.
|
||||
@@ -1,134 +1,147 @@
|
||||
# Tutorial de aula — plugin Ping
|
||||
|
||||
Material para o aluno implementar um plugin **Ping/Pong** com medição de RTT (round-trip time). A solução em Java **não** entra no repositório oficial; apenas este tutorial e a pasta [`plugins/ping/`](../../plugins/ping/) (README + `.gitignore`).
|
||||
Exercício para montar o plugin **mais simples** do Pitiupi: medir **RTT** (tempo de ida e volta) com mensagens `Ping` / `Pong` na rede, igual a um `ping` de laboratório — mas usando o **mesmo mecanismo de plugins** do chat e do screencast.
|
||||
|
||||
Índice geral: [README.md](README.md).
|
||||
A solução Java **não** entra no Git: só este tutorial e [`plugins/ping/README.md`](../../plugins/ping/README.md). O professor valida localmente; o aluno implementa no próprio `plugins/ping/` (gitignored).
|
||||
|
||||
## Objetivos
|
||||
Índice: [README.md](README.md) · Plugins de referência: [11_plugins_referencia.md](11_plugins_referencia.md).
|
||||
|
||||
- Estender `Plugin` e registrar SPI.
|
||||
- Mensagens `Serializable` que estendem `Message`.
|
||||
- `ConnectionManager`: multicast UDP, unicast UDP e TCP.
|
||||
- UI na aba central: `JPanel` + `bindShellWorkspaceContent` (sem `JFrame`); abrir pela galeria **Plugins** ou peer actions.
|
||||
- Instalar JAR em `PITIUPI_HOME/plugins` e testar com um colega.
|
||||
## O que você vai aprender
|
||||
|
||||
| Componente do núcleo | Para que serve no Ping |
|
||||
| --- | --- |
|
||||
| `Plugin` + SPI | O Pitiupi descobre seu JAR e chama `createPlugin` |
|
||||
| `Message` serializável | `PingMessage`, `PongMessage`, `PingStopMessage` |
|
||||
| `getMessages()` / `receiveMessage` | O núcleo entrega só os tipos que você registrou |
|
||||
| `MessageDispatcher` | Envia UDP unicast/multicast sem montar socket manual |
|
||||
| `PluginWorkspace` | Galeria e peer actions abrem a aba central |
|
||||
| `PeerAction` | Menu ao clicar em um usuário na lista lateral |
|
||||
| `SessionTabBar` | Abas por peer (passivas ao receber, como no chat) |
|
||||
|
||||
## Limitações didáticas (de propósito)
|
||||
|
||||
- Não é ICMP do sistema operacional — são mensagens Java na LAN.
|
||||
- Sem gráficos nem histórico em arquivo.
|
||||
- Transporte padrão: **UDP unicast** para um peer; multicast só no “ping para todos (uma vez)”.
|
||||
- Não há moderação, login extra nem criptografia.
|
||||
|
||||
## Pré-requisitos
|
||||
|
||||
- Pitiupi com login (nome + porta).
|
||||
- [09_sistema_de_plugins.md](09_sistema_de_plugins.md).
|
||||
- JDK, Ant; núcleo em `pitiupi/dist/pitiupi.jar`.
|
||||
|
||||
## Estrutura sugerida (projeto do aluno)
|
||||
|
||||
```
|
||||
plugins/ping/ # ou projeto fora do repo oficial
|
||||
├── build.xml
|
||||
├── nbproject/
|
||||
├── src/
|
||||
│ ├── META-INF/services/pitiupi.models.Plugin
|
||||
│ └── br/ufsj/redes/ping/
|
||||
│ ├── PingPlugin.java
|
||||
│ ├── PingMessage.java
|
||||
│ ├── PongMessage.java
|
||||
│ └── PingPanel.java
|
||||
└── dist/pitiupi.ping.jar
|
||||
```
|
||||
|
||||
Modelo Ant: copiar [`plugins/chat`](../chat) e ajustar classpath para `../../pitiupi/dist/pitiupi.jar`.
|
||||
1. Pitiupi abrindo com login (nome + porta).
|
||||
2. [`09_sistema_de_plugins.md`](09_sistema_de_plugins.md).
|
||||
3. JDK 17 + Ant.
|
||||
4. Núcleo compilado: `pitiupi/dist/pitiupi.jar`.
|
||||
|
||||
## Passo 1 — Projeto Ant
|
||||
|
||||
1. Copiar `plugins/chat`.
|
||||
2. JAR de saída: `dist/pitiupi.ping.jar`.
|
||||
3. Classpath de compilação: JAR do núcleo.
|
||||
1. Copie `plugins/chat` para `plugins/ping` (somente na sua máquina).
|
||||
2. Renomeie o projeto para `pitiupi.ping` e o JAR de saída para `dist/pitiupi.ping.jar`.
|
||||
3. Classpath de compilação: `../../pitiupi/dist/pitiupi.jar`.
|
||||
|
||||
**Verificação:** `ant jar` sem erros.
|
||||
|
||||
## Passo 2 — `PingPlugin`
|
||||
## Passo 2 — SPI
|
||||
|
||||
| Método | Exemplo |
|
||||
| --- | --- |
|
||||
| `getName()` | `"Ping"` |
|
||||
| `getDisplayName()` | `"Ping na rede"` |
|
||||
| `getIdentifier()` | `"pitiupi.ping"` (único na sala) |
|
||||
Arquivo `src/META-INF/services/pitiupi.models.Plugin`:
|
||||
|
||||
Em `createPlugin(ConnectionManager cm, PluginWorkspace workspace)`:
|
||||
|
||||
```java
|
||||
pingPanel = new PingPanel(cm);
|
||||
bindShellWorkspaceContent(pingPanel);
|
||||
installPeerActions(workspace.peerActions());
|
||||
```
|
||||
|
||||
**Não** chame `openOrFocusTab` no boot — a aba abre quando o usuário clicar na galeria ou usar peer action.
|
||||
|
||||
Peer actions sugeridas: “Enviar ping” (peer único), “Ping para todos”.
|
||||
|
||||
**Verificação:** após instalar o JAR e reiniciar, só **Plugins** e **Configurações** no boot; botão na galeria abre a aba Ping.
|
||||
pitiupi.ping.plugin.PingPlugin
|
||||
```
|
||||
|
||||
## Passo 3 — Mensagens
|
||||
|
||||
`PingMessage` e `PongMessage` com `pingId` (UUID) e timestamps para RTT.
|
||||
Três classes em `pitiupi.ping.net`, todas estendendo `pitiupi.models.Messages.Message`:
|
||||
|
||||
Registrar em `getMessages()`:
|
||||
1. **`PingMessage`** — campos: `pingId` (UUID), `sentAtMillis`, `senderName`, `senderIp`, `mode` (`ONCE` ou `CONTINUOUS_TICK`), `sessionId` (para série contínua).
|
||||
2. **`PongMessage`** — `pingId`, `echoSentAtMillis` (copiado do ping), `responderName`, `responderIp`.
|
||||
3. **`PingStopMessage`** — `sessionId`, `senderName`, `senderIp` (avisa que o contínuo parou).
|
||||
|
||||
Registre em `PingPlugin.getMessages()`:
|
||||
|
||||
```java
|
||||
return List.of(PingMessage.class, PongMessage.class);
|
||||
return List.of(PingMessage.class, PongMessage.class, PingStopMessage.class);
|
||||
```
|
||||
|
||||
Em `receiveMessage`: ao receber ping, responder pong; ao receber pong, calcular RTT e atualizar a UI com `SwingUtilities.invokeLater`.
|
||||
**Por quê:** o núcleo só desserializa tipos declarados; evita que outro plugin receba seus bytes por engano.
|
||||
|
||||
## Passo 4 — `PingPanel`
|
||||
## Passo 4 — `PingController` (lógica)
|
||||
|
||||
Botões para multicast, unicast UDP e TCP; área de log ou label com último RTT em ms. Para um peer, use campo IP como no `ChatPanel` ou peer action na lista lateral.
|
||||
Classe em `pitiupi.ping.control` **sem Swing**:
|
||||
|
||||
## Passo 5 — Transportes
|
||||
- Ao **enviar** ping: grave `pendingByPingId.put(pingId, sentAtMillis)` e use `MessageDispatcher.send(..., UNICAST_UDP)`.
|
||||
- Ao **receber** `PingMessage`: responda com `PongMessage` (eco do `sentAtMillis`).
|
||||
- Ao **receber** `PongMessage`: `rtt = now - pending.remove(pingId)` e notifique a UI.
|
||||
- **Contínuo:** `javax.swing.Timer` a cada 1 s envia `CONTINUOUS_TICK` com o mesmo `sessionId`.
|
||||
- **Parar:** cancele o timer local e envie `PingStopMessage` ao peer.
|
||||
|
||||
Use `pitiupi.application.messaging.MessageDispatcher` no núcleo:
|
||||
Use `PeerIpv4` e `LocalNetworkInfo.bestLocalIpv4()` para IP legível no Windows.
|
||||
|
||||
## Passo 5 — `PingPanel` (UI)
|
||||
|
||||
`JPanel` com:
|
||||
|
||||
- `SessionTabBar` — aba fixa **Todos** + `addTabIfAbsent` quando chega tráfego de um peer (**sem** mudar a aba ativa da shell).
|
||||
- `JTextArea` monoespaçada por aba; linhas como `[HH:mm:ss] pong ← Alice · RTT 42 ms`.
|
||||
- Botão **Parar todos os pings contínuos** chamando o controller.
|
||||
|
||||
**Não** use `JFrame`. No `PingPlugin`:
|
||||
|
||||
```java
|
||||
MessageDispatcher.send(connectionManager, pingMessage, SendTarget.broadcastAll(), TransportMode.MULTICAST_UDP);
|
||||
MessageDispatcher.send(connectionManager, pingMessage, SendTarget.singlePeer(addr), TransportMode.UNICAST_UDP);
|
||||
MessageDispatcher.send(connectionManager, pingMessage, SendTarget.singlePeer(addr), TransportMode.TCP);
|
||||
bindShellWorkspaceContent(panel);
|
||||
```
|
||||
|
||||
| Destino | Transporte didático | Observação |
|
||||
| --- | --- | --- |
|
||||
| Todos | Multicast UDP | Padrão em sala |
|
||||
| Todos | Unicast UDP | Loop em cada peer online (demo) |
|
||||
| Um peer | Unicast UDP / TCP | IP da lista ou manual |
|
||||
A aba só abre quando o usuário clicar na galeria **Plugins** ou numa peer action.
|
||||
|
||||
Não alterar o endereço multicast do núcleo (`224.0.0.3`).
|
||||
## Passo 6 — `PingPlugin`
|
||||
|
||||
## Passo 6 — SPI e instalação
|
||||
|
||||
`META-INF/services/pitiupi.models.Plugin`:
|
||||
|
||||
```
|
||||
br.ufsj.redes.ping.PingPlugin
|
||||
```
|
||||
|
||||
Copiar JAR para `~/pitiupi/plugins/` (ou instalar pela aba Configurações) e **reiniciar**.
|
||||
|
||||
## Critérios de sucesso
|
||||
|
||||
| # | Critério |
|
||||
| Método | Valor |
|
||||
| --- | --- |
|
||||
| C1 | JAR carrega sem exceção |
|
||||
| C2 | Pelo menos um transporte funciona entre duas instâncias |
|
||||
| C3 | RTT em ms após o pong |
|
||||
| C4 | README do aluno com build e transporte usado |
|
||||
| `getIdentifier()` | `pitiupi.ping` (único na sala) |
|
||||
| `getDisplayName()` | `Ping na rede` |
|
||||
|
||||
## Anti-padrões
|
||||
`createPlugin(ConnectionManager, PluginWorkspace)`:
|
||||
|
||||
- Commitar solução no repo oficial.
|
||||
- Busy-wait na EDT.
|
||||
- Objetos não `Serializable`.
|
||||
- Mesmo `getIdentifier()` que outro plugin.
|
||||
1. Crie `PingController` e `PingPanel`.
|
||||
2. `bindShellWorkspaceContent(panel)`.
|
||||
3. `installPeerActions(workspace.peerActions())`.
|
||||
|
||||
## Referências
|
||||
**Peer actions sugeridas:**
|
||||
|
||||
- [10_tutorial_plugin.md](10_tutorial_plugin.md)
|
||||
- [05_udp_multicast.md](05_udp_multicast.md)
|
||||
- [06_tcp_conexoes.md](06_tcp_conexoes.md)
|
||||
- [04_inicializacao_e_gui.md](04_inicializacao_e_gui.md)
|
||||
| id | Label | Alvo |
|
||||
| --- | --- | --- |
|
||||
| `ping.once` | Ping uma vez | um peer |
|
||||
| `ping.continuous` | Ping contínuo | um peer |
|
||||
| `ping.stop` | Parar ping | um peer |
|
||||
| `ping.all.once` | Ping para todos (uma vez) | Todos → `MULTICAST_UDP` |
|
||||
|
||||
Em cada ação de peer único: `workspace.openOrFocusTab(getIdentifier(), getDisplayName())` e chame o controller.
|
||||
|
||||
`receiveMessage`: despache para o controller com `SwingUtilities.invokeLater`.
|
||||
|
||||
`onShellTabClosed`: pare todos os timers contínuos.
|
||||
|
||||
## Passo 7 — Instalar e testar
|
||||
|
||||
1. `ant jar` → `dist/pitiupi.ping.jar`.
|
||||
2. Aba **Configurações** → **Instalar JAR…** (ou copie para `~/pitiupi/plugins/`).
|
||||
3. **Reinicie** o Pitiupi (obrigatório após mudar plugins).
|
||||
4. Dois processos no mesmo PC: portas **5000** e **5001**, nomes diferentes.
|
||||
5. Peer action **Ping uma vez** → veja RTT no log.
|
||||
6. **Ping contínuo** → linhas a cada ~1 s; **Parar ping** → o outro vê “parou o ping contínuo”.
|
||||
7. Com colega na LAN: mesma porta, JAR instalado nos dois.
|
||||
|
||||
Instalação usa o mesmo `PluginStore` da aba **Configurações** (`installFromFile` + reinício). O núcleo expõe `pitiupi.tools.PluginStoreCli` para testes sem interface gráfica.
|
||||
|
||||
## Passo 8 — Comparar com o chat
|
||||
|
||||
O chat usa a mesma ideia de abas de sessão, mas adiciona roteamento TCP privado e HTML. No Ping mantenha **só** UDP e texto em `JTextArea` — ideal para entender o fluxo antes de copiar padrões para um plugin novo.
|
||||
|
||||
## Checklist do aluno
|
||||
|
||||
- [ ] SPI e `getIdentifier` únicos
|
||||
- [ ] Três mensagens registradas
|
||||
- [ ] RTT calculado no `Pong`
|
||||
- [ ] Aba passiva ao receber ping de peer novo
|
||||
- [ ] Contínuo + parar com aviso ao peer
|
||||
- [ ] Reinício após instalar JAR
|
||||
- [ ] Teste com segundo usuário (5001 ou outro PC)
|
||||
@@ -1,72 +0,0 @@
|
||||
/*
|
||||
* To change this license header, choose License Headers in Project Properties.
|
||||
* To change this template file, choose Tools | Templates
|
||||
* and open the template in the editor.
|
||||
*/
|
||||
package pitiupi.GUI;
|
||||
|
||||
import javax.swing.*;
|
||||
|
||||
/**
|
||||
* Janela que exibe informações sobre a aplicação, incluindo versão,
|
||||
* descrição e autores.
|
||||
*
|
||||
* @author gustavo
|
||||
*/
|
||||
|
||||
public class About extends javax.swing.JFrame {
|
||||
public About () {
|
||||
this.setDefaultCloseOperation(javax.swing.WindowConstants.HIDE_ON_CLOSE);
|
||||
initComponents();
|
||||
}
|
||||
|
||||
private void initComponents() {
|
||||
this.setTitle("About");
|
||||
this.setBounds(new java.awt.Rectangle(0, 0, 800, 600));
|
||||
this.getContentPane().setLayout(new java.awt.BorderLayout(5, 5));
|
||||
this.setPreferredSize(new java.awt.Dimension(800, 600));
|
||||
|
||||
JScrollPane scroll = getJScrollPane();
|
||||
|
||||
this.add(scroll);
|
||||
}
|
||||
|
||||
private static JScrollPane getJScrollPane() {
|
||||
JEditorPane texto = new JEditorPane();
|
||||
texto.setContentType("text/html");
|
||||
texto.setEditable(false);
|
||||
|
||||
texto.setText("""
|
||||
<html>
|
||||
<body>
|
||||
<h1>Share My Sheet</h1>
|
||||
\s
|
||||
<p>
|
||||
<b>Versão:</b> 1.0
|
||||
</p>
|
||||
\s
|
||||
<p>
|
||||
Plataforma P2P baseada em plugins para o desenvolvimento de aplicações
|
||||
distribuídas. Criada para apoiar as práticas da disciplina de Redes de
|
||||
Computadores e servir como base para projetos acadêmicos e TCCs.
|
||||
\s
|
||||
</p>
|
||||
\s
|
||||
<p>
|
||||
<b>Autores:</b><br>
|
||||
Flávio<br>
|
||||
Tony<br>
|
||||
Gustavo Henrique
|
||||
</p>
|
||||
\s
|
||||
<p>
|
||||
Universidade Federal de São João del-Rei (UFSJ)
|
||||
</p>
|
||||
\s
|
||||
</body>
|
||||
</html>
|
||||
\s""");
|
||||
|
||||
return new JScrollPane(texto);
|
||||
}
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
package pitiupi.GUI;
|
||||
|
||||
import java.awt.BorderLayout;
|
||||
import java.awt.FlowLayout;
|
||||
import java.awt.GridBagConstraints;
|
||||
import java.awt.GridBagLayout;
|
||||
import java.awt.Insets;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.util.prefs.Preferences;
|
||||
|
||||
import javax.swing.JButton;
|
||||
import javax.swing.JDialog;
|
||||
import javax.swing.JLabel;
|
||||
import javax.swing.JPanel;
|
||||
import javax.swing.JSpinner;
|
||||
import javax.swing.JTextField;
|
||||
|
||||
/**
|
||||
* Janela de preferencias da aplicação.
|
||||
*
|
||||
* <p>Permite ao usuário informar o nome utilizado na rede e a porta
|
||||
* multicast que será utilizada pela comunicação.</p>
|
||||
*
|
||||
* @author flavio
|
||||
* @author tony
|
||||
*/
|
||||
public final class Login extends JDialog {
|
||||
|
||||
private static final Preferences prefs = Preferences.userNodeForPackage(Login.class);
|
||||
private static final String PREF_NOME = "lastUserName";
|
||||
private static final String PREF_PORTA = "lastPort";
|
||||
|
||||
JLabel nameLabel = new JLabel("Name: ");
|
||||
JLabel portLabel = new JLabel("Port: ");
|
||||
|
||||
JTextField nameField = new JTextField();
|
||||
JSpinner portField = new JSpinner();
|
||||
|
||||
JButton okButton = new JButton("OK");
|
||||
JButton cancelButton = new JButton("Cancel");
|
||||
|
||||
public Login() {
|
||||
initComponents();
|
||||
this.setSize(400, 150);
|
||||
this.setModal(true);
|
||||
}
|
||||
|
||||
public void initComponents() {
|
||||
this.setTitle("Login");
|
||||
|
||||
String nomeSalvo = prefs.get(PREF_NOME, System.getProperty("user.name"));
|
||||
int portaSalva = prefs.getInt(PREF_PORTA, 8000);
|
||||
|
||||
portField.setModel(new javax.swing.SpinnerNumberModel(portaSalva, 8000, 9000, 1));
|
||||
nameField.setText(nomeSalvo);
|
||||
|
||||
JPanel topPanel = new JPanel(new GridBagLayout());
|
||||
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
|
||||
|
||||
buttonPanel.add(okButton);
|
||||
buttonPanel.add(cancelButton);
|
||||
|
||||
GridBagConstraints gbc = new GridBagConstraints();
|
||||
|
||||
gbc.insets = new Insets(4, 4, 4, 4);
|
||||
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = 0;
|
||||
gbc.weightx = 0;
|
||||
topPanel.add(nameLabel, gbc);
|
||||
|
||||
gbc.gridx = 1;
|
||||
gbc.gridy = 0;
|
||||
gbc.fill = GridBagConstraints.HORIZONTAL;
|
||||
gbc.weightx = 1;
|
||||
topPanel.add(nameField, gbc);
|
||||
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = 1;
|
||||
gbc.fill = GridBagConstraints.NONE;
|
||||
gbc.weightx = 0;
|
||||
topPanel.add(portLabel, gbc);
|
||||
|
||||
gbc.fill = GridBagConstraints.HORIZONTAL;
|
||||
gbc.gridx = 1;
|
||||
gbc.gridy = 1;
|
||||
gbc.weightx = 1;
|
||||
topPanel.add(portField, gbc);
|
||||
|
||||
this.add(topPanel);
|
||||
this.add(buttonPanel, BorderLayout.SOUTH);
|
||||
|
||||
okButton.addActionListener((ActionEvent e) -> {
|
||||
try {
|
||||
portField.commitEdit();
|
||||
} catch (java.text.ParseException ex) {
|
||||
return;
|
||||
}
|
||||
|
||||
prefs.put(PREF_NOME, nameField.getText());
|
||||
prefs.putInt(PREF_PORTA, (Integer) portField.getValue());
|
||||
|
||||
Login.this.setVisible(false);
|
||||
});
|
||||
|
||||
cancelButton.addActionListener((ActionEvent e) -> {
|
||||
System.exit(0);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return nameField.getText();
|
||||
}
|
||||
|
||||
public int getPort() {
|
||||
return ((Integer) portField.getValue());
|
||||
}
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
/*
|
||||
* To change this license header, choose License Headers in Project Properties.
|
||||
* To change this template file, choose Tools | Templates
|
||||
* and open the template in the editor.
|
||||
*/
|
||||
package pitiupi.GUI;
|
||||
|
||||
import pitiupi.control.ConnectionManager;
|
||||
import pitiupi.control.GuiManager;
|
||||
import pitiupi.control.plugins.PluginManager;
|
||||
import pitiupi.presentation.shell.NetworkUsersPanel;
|
||||
import pitiupi.presentation.shell.ShellHost;
|
||||
import pitiupi.presentation.shell.ShellMenuBar;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.awt.event.WindowAdapter;
|
||||
import java.awt.event.WindowEvent;
|
||||
|
||||
/**
|
||||
* @deprecated Substituído por {@link pitiupi.presentation.shell.MainShellFrame}.
|
||||
*/
|
||||
@Deprecated
|
||||
public class MainWindow extends JFrame implements ShellHost {
|
||||
|
||||
private JLabel status;
|
||||
private final NetworkUsersPanel networkUsersPanel;
|
||||
|
||||
private final ConnectionManager connectionManager;
|
||||
private final PluginManager pluginManager;
|
||||
private final GuiManager guiManager;
|
||||
|
||||
public MainWindow(String userName, int port) {
|
||||
pluginManager = new PluginManager();
|
||||
connectionManager = new ConnectionManager(userName, pluginManager, port);
|
||||
|
||||
ShellMenuBar menuBar = new ShellMenuBar();
|
||||
setJMenuBar(menuBar);
|
||||
setDefaultCloseOperation(javax.swing.WindowConstants.DO_NOTHING_ON_CLOSE);
|
||||
setTitle("Pitiupi");
|
||||
|
||||
networkUsersPanel = new NetworkUsersPanel(connectionManager);
|
||||
getContentPane().add(new JScrollPane(new JTabbedPane()), BorderLayout.CENTER);
|
||||
getContentPane().add(networkUsersPanel, BorderLayout.LINE_END);
|
||||
|
||||
initStatusPanel();
|
||||
|
||||
guiManager = new GuiManager(this, menuBar, networkUsersPanel);
|
||||
|
||||
loadPlugins();
|
||||
|
||||
addWindowListener(new WindowAdapter() {
|
||||
@Override
|
||||
public void windowClosing(WindowEvent e) {
|
||||
exit();
|
||||
}
|
||||
});
|
||||
|
||||
pack();
|
||||
setSize(new Dimension(1024, 768));
|
||||
}
|
||||
|
||||
private void initStatusPanel() {
|
||||
JPanel statusPanel = new JPanel();
|
||||
status = new JLabel();
|
||||
statusPanel.setLayout(new FlowLayout(FlowLayout.RIGHT));
|
||||
updateStatus();
|
||||
statusPanel.add(status);
|
||||
getContentPane().add(statusPanel, BorderLayout.PAGE_END);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exit() {
|
||||
connectionManager.shutdownGracefully();
|
||||
setVisible(false);
|
||||
dispose();
|
||||
System.exit(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ConnectionManager getConnectionManager() {
|
||||
return connectionManager;
|
||||
}
|
||||
|
||||
public void loadPlugins() {
|
||||
pluginManager.loadPlugins(connectionManager, guiManager);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUserName(String userName) {
|
||||
connectionManager.setUserName(userName);
|
||||
updateStatus();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setStatusMessage(String message) {
|
||||
status.setText(message);
|
||||
}
|
||||
|
||||
private void updateStatus() {
|
||||
status.setText("Connected on port " + connectionManager.getPort() + " name " + connectionManager.getUserName());
|
||||
}
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
/*
|
||||
* To change this license header, choose License Headers in Project Properties.
|
||||
* To change this template file, choose Tools | Templates
|
||||
* and open the template in the editor.
|
||||
*/
|
||||
package pitiupi.GUI;
|
||||
|
||||
import pitiupi.models.Menus.MenuCategory;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.InputEvent;
|
||||
import java.awt.event.KeyEvent;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Barra de menus principal da aplicação.
|
||||
*
|
||||
* <p>Gerencia os menus padrões da interface e disponibiliza um espaço
|
||||
* para que plugins adicionem seus próprios menus.</p>
|
||||
*
|
||||
* @author flavio
|
||||
* @author Gustavo
|
||||
* @author tony
|
||||
*
|
||||
*/
|
||||
public class MenuBar extends JMenuBar {
|
||||
|
||||
private JMenuItem preferencesMenu;
|
||||
private JMenuItem aboutMenu;
|
||||
private JMenu fileMenu;
|
||||
private JMenu helpMenu;
|
||||
private JMenu pluginMenu;
|
||||
private Map<MenuCategory, JMenu> categories;
|
||||
|
||||
private JMenuItem exitMenu;
|
||||
private final MainWindow mainWindow;
|
||||
|
||||
/**
|
||||
* Cria a barra de menus principal da aplicação.
|
||||
*
|
||||
* @param mainWindow mainWindow janela principal da aplicação utilizada para executar
|
||||
*/
|
||||
public MenuBar(MainWindow mainWindow) {
|
||||
this.mainWindow = mainWindow;
|
||||
this.initComponents();
|
||||
}
|
||||
|
||||
private void initComponents() {
|
||||
initFileMenu();
|
||||
initPluginMenu();
|
||||
initHelpMenu();
|
||||
}
|
||||
|
||||
private void initFileMenu() {
|
||||
fileMenu = new JMenu("File");
|
||||
exitMenu = new JMenuItem("Exit");
|
||||
|
||||
exitMenu.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F4, InputEvent.ALT_DOWN_MASK));
|
||||
exitMenu.addActionListener(this::exitMenuActionPerformed);
|
||||
fileMenu.add(exitMenu);
|
||||
this.add(fileMenu);
|
||||
}
|
||||
|
||||
private void initPluginMenu() {
|
||||
pluginMenu = new JMenu("Plugin");
|
||||
categories = new HashMap<>();
|
||||
|
||||
for (MenuCategory category : MenuCategory.values()) {
|
||||
JMenu newCategoryMenu = new JMenu(category.toString());
|
||||
|
||||
pluginMenu.add(newCategoryMenu);
|
||||
categories.put(category, newCategoryMenu);
|
||||
}
|
||||
|
||||
this.add(pluginMenu);
|
||||
}
|
||||
|
||||
private void initHelpMenu() {
|
||||
helpMenu = new JMenu("Help");
|
||||
preferencesMenu = new JMenuItem("Preferences");
|
||||
aboutMenu = new JMenuItem("About");
|
||||
|
||||
preferencesMenu.addActionListener(this::setupMenuActionPerformed);
|
||||
helpMenu.add(preferencesMenu);
|
||||
|
||||
aboutMenu.addActionListener(this::aboutMenuActionPerformed);
|
||||
helpMenu.add(aboutMenu);
|
||||
|
||||
this.add(helpMenu);
|
||||
}
|
||||
|
||||
private void exitMenuActionPerformed(ActionEvent evt) {
|
||||
mainWindow.exit();
|
||||
}
|
||||
|
||||
private void setupMenuActionPerformed(ActionEvent evt) {
|
||||
Login login = new Login();
|
||||
login.setVisible(true);
|
||||
|
||||
mainWindow.setUserName(login.getName());
|
||||
mainWindow.getConnectionManager().setPort(login.getPort());
|
||||
|
||||
login.dispose();
|
||||
}
|
||||
|
||||
private void aboutMenuActionPerformed(ActionEvent evt) {
|
||||
About about = new About();
|
||||
about.setVisible(true);
|
||||
}
|
||||
|
||||
public void addPluginMenu(MenuCategory category, JMenuItem menu){
|
||||
categories.get(category).add(menu);
|
||||
}
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
package pitiupi.GUI;
|
||||
|
||||
import java.awt.BorderLayout;
|
||||
import java.awt.Dimension;
|
||||
import java.awt.event.MouseAdapter;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
import javax.swing.*;
|
||||
|
||||
import pitiupi.models.Menus.MenuCategory;
|
||||
import pitiupi.models.User;
|
||||
import pitiupi.control.PeerListener;
|
||||
|
||||
/**
|
||||
* Painel da interface gráfica responsável por exibir os usuários ativos
|
||||
* na rede.
|
||||
*
|
||||
* @author tony
|
||||
*/
|
||||
public class OnlineUsersPanel extends JPanel implements PeerListener {
|
||||
|
||||
private final DefaultListModel<User> listModel;
|
||||
private final JList<User> userList;
|
||||
JPopupMenu menu;
|
||||
private Map<MenuCategory, JMenu> categories;
|
||||
|
||||
public OnlineUsersPanel() {
|
||||
menu = new JPopupMenu();
|
||||
categories = new HashMap<>();
|
||||
initMenu();
|
||||
|
||||
setLayout(new BorderLayout());
|
||||
setBorder(BorderFactory.createTitledBorder("Usuários nesta rede"));
|
||||
setPreferredSize(new Dimension(160, 0));
|
||||
|
||||
listModel = new DefaultListModel<>();
|
||||
userList = new JList<>(listModel);
|
||||
add(new JScrollPane(userList), BorderLayout.CENTER);
|
||||
|
||||
setupUserListActions();
|
||||
}
|
||||
|
||||
private void initMenu() {
|
||||
for (MenuCategory category : MenuCategory.values()) {
|
||||
JMenu newCategory = new JMenu(category.toString());
|
||||
|
||||
menu.add(newCategory);
|
||||
categories.put(category, newCategory);
|
||||
}
|
||||
}
|
||||
|
||||
private void setupUserListActions() {
|
||||
userList.addMouseListener(new MouseAdapter() {
|
||||
|
||||
@Override
|
||||
public void mouseClicked(MouseEvent e) {
|
||||
int index = userList.locationToIndex(e.getPoint());
|
||||
|
||||
if (index == -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
userList.setSelectedIndex(index);
|
||||
User selected = listModel.getElementAt(index);
|
||||
if (selected != null && selected.isBroadcastTarget()) {
|
||||
return;
|
||||
}
|
||||
menu.show(userList, e.getX(), e.getY());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void initUnicastMenu(JMenuItem menuItem, Consumer<User> action) {
|
||||
menuItem.addActionListener(e -> {
|
||||
User user = userList.getSelectedValue();
|
||||
|
||||
if (user != null && !user.isBroadcastTarget() && user.getAddress() != null) {
|
||||
action.accept(user);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void addUnicastMenu(MenuCategory category, JMenuItem menuItem) {
|
||||
categories.get(category).add(menuItem);
|
||||
}
|
||||
|
||||
/**
|
||||
* Atualiza a lista de usuários exibida no painel.
|
||||
*
|
||||
* @param peers lista atualizada de usuários ativos na rede.
|
||||
*/
|
||||
@Override
|
||||
public void onPeersChanged(List<User> peers) {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
listModel.clear();
|
||||
listModel.addElement(User.broadcastAll());
|
||||
for (User peer : peers) {
|
||||
listModel.addElement(peer);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -34,13 +34,13 @@ public class Main {
|
||||
}
|
||||
}
|
||||
} catch (ClassNotFoundException ex) {
|
||||
java.util.logging.Logger.getLogger(pitiupi.GUI.MainWindow.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
|
||||
Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, ex);
|
||||
} catch (InstantiationException ex) {
|
||||
java.util.logging.Logger.getLogger(pitiupi.GUI.MainWindow.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
|
||||
Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, ex);
|
||||
} catch (IllegalAccessException ex) {
|
||||
java.util.logging.Logger.getLogger(pitiupi.GUI.MainWindow.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
|
||||
Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, ex);
|
||||
} catch (javax.swing.UnsupportedLookAndFeelException ex) {
|
||||
java.util.logging.Logger.getLogger(pitiupi.GUI.MainWindow.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
|
||||
Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, ex);
|
||||
}
|
||||
|
||||
java.awt.EventQueue.invokeLater(new Runnable() {
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
package pitiupi.api.plugin;
|
||||
|
||||
import pitiupi.application.invites.InviteInboxService;
|
||||
import pitiupi.application.peeractions.PeerActionBroker;
|
||||
import pitiupi.application.peeractions.PluginPeerActionsRegistrar;
|
||||
import pitiupi.control.ConnectionManager;
|
||||
import pitiupi.control.GuiManager;
|
||||
import pitiupi.models.Plugin;
|
||||
|
||||
import javax.swing.JComponent;
|
||||
|
||||
/**
|
||||
* Adaptador Onda C: expõe workspace e mantém {@link GuiManager} para plugins legados.
|
||||
*/
|
||||
public final class LegacyPluginWorkspace implements PluginWorkspace {
|
||||
|
||||
private final String pluginId;
|
||||
private final ConnectionManager connectionManager;
|
||||
private final GuiManager guiManager;
|
||||
private final PeerActionBroker peerActionBroker;
|
||||
private final InviteInboxService inviteInboxService;
|
||||
private final Plugin plugin;
|
||||
private final PluginPeerActionsRegistrar registrar;
|
||||
|
||||
public LegacyPluginWorkspace(
|
||||
String pluginId,
|
||||
Plugin plugin,
|
||||
ConnectionManager connectionManager,
|
||||
GuiManager guiManager,
|
||||
PeerActionBroker peerActionBroker,
|
||||
InviteInboxService inviteInboxService
|
||||
) {
|
||||
this.pluginId = pluginId;
|
||||
this.plugin = plugin;
|
||||
this.connectionManager = connectionManager;
|
||||
this.guiManager = guiManager;
|
||||
this.peerActionBroker = peerActionBroker;
|
||||
this.inviteInboxService = inviteInboxService;
|
||||
this.registrar = new PluginPeerActionsRegistrar(peerActionBroker, pluginId);
|
||||
peerActionBroker.registerPlugin(pluginId);
|
||||
}
|
||||
|
||||
public GuiManager guiManager() {
|
||||
return guiManager;
|
||||
}
|
||||
|
||||
public String pluginId() {
|
||||
return pluginId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ConnectionManager connectionManager() {
|
||||
return connectionManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PeerActionsRegistrar peerActions() {
|
||||
return registrar;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void openOrFocusTab(String pluginInstanceId, String title, JComponent content) {
|
||||
peerActionBroker.setActivePlugin(pluginInstanceId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setStatusMessage(String text) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deliverInviteAccepted(InviteDetails invite) {
|
||||
plugin.onInviteAccepted(invite);
|
||||
}
|
||||
|
||||
public void bindInviteDelivery() {
|
||||
inviteInboxService.setInviteHandler(pluginId, plugin::onInviteAccepted);
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* Pendências de atividade por peer: contagem (chat) ou indicador (screencast).
|
||||
* Pendências de atividade por peer (núcleo). Badges na lista lateral adiados na v1 — D23.
|
||||
*/
|
||||
public final class PeerActivityInbox {
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package pitiupi.control.plugins;
|
||||
|
||||
import pitiupi.Main;
|
||||
import pitiupi.api.plugin.LegacyPluginWorkspace;
|
||||
import pitiupi.api.plugin.PluginWorkspace;
|
||||
import pitiupi.models.Plugin;
|
||||
import pitiupi.control.ConnectionManager;
|
||||
@@ -111,19 +110,6 @@ public class PluginManager {
|
||||
}
|
||||
}
|
||||
|
||||
/** @deprecated use {@link #loadPlugins(ConnectionManager, GuiManager, Function)} com {@link pitiupi.api.plugin.ShellPluginWorkspace} */
|
||||
@Deprecated
|
||||
public void loadPlugins(ConnectionManager connectionManager, GuiManager guiManager) {
|
||||
loadPlugins(connectionManager, guiManager, plugin -> new LegacyPluginWorkspace(
|
||||
plugin.getIdentifier(),
|
||||
plugin,
|
||||
connectionManager,
|
||||
guiManager,
|
||||
connectionManager.getPeerActionBroker(),
|
||||
connectionManager.getInviteInboxService()
|
||||
));
|
||||
}
|
||||
|
||||
public void loadPlugins(
|
||||
ConnectionManager connectionManager,
|
||||
GuiManager guiManager,
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package pitiupi.presentation.shell;
|
||||
|
||||
import javax.swing.JEditorPane;
|
||||
import javax.swing.JFrame;
|
||||
import javax.swing.JScrollPane;
|
||||
import javax.swing.WindowConstants;
|
||||
|
||||
/**
|
||||
* Diálogo simples "Sobre" (Ajuda).
|
||||
*/
|
||||
public final class AboutDialog extends JFrame {
|
||||
|
||||
public AboutDialog() {
|
||||
setDefaultCloseOperation(WindowConstants.HIDE_ON_CLOSE);
|
||||
setTitle("Sobre o Pitiupi");
|
||||
setSize(520, 360);
|
||||
UiGeometry.centerOnScreen(this);
|
||||
|
||||
JEditorPane texto = new JEditorPane();
|
||||
texto.setContentType("text/html");
|
||||
texto.setEditable(false);
|
||||
texto.setText("""
|
||||
<html><body style='font-family:sans-serif'>
|
||||
<h2>Pitiupi</h2>
|
||||
<p>Plataforma P2P didática com plugins (chat, compartilhamento de tela, exercício ping).</p>
|
||||
<p><b>Uso em laboratório:</b> UFSJ / Redes de Computadores.</p>
|
||||
</body></html>
|
||||
""");
|
||||
|
||||
add(new JScrollPane(texto));
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
package pitiupi.presentation.shell;
|
||||
|
||||
import pitiupi.GUI.About;
|
||||
|
||||
import javax.swing.JMenu;
|
||||
import javax.swing.JMenuBar;
|
||||
import javax.swing.JMenuItem;
|
||||
@@ -14,7 +12,7 @@ public final class ShellMenuBar extends JMenuBar {
|
||||
public ShellMenuBar() {
|
||||
JMenu help = new JMenu("Ajuda");
|
||||
JMenuItem about = new JMenuItem("Sobre");
|
||||
about.addActionListener(e -> new About().setVisible(true));
|
||||
about.addActionListener(e -> new AboutDialog().setVisible(true));
|
||||
help.add(about);
|
||||
add(help);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
package pitiupi.tools;
|
||||
|
||||
import pitiupi.errors.PitiupiException;
|
||||
import pitiupi.platform.PitiupiHome;
|
||||
import pitiupi.plugins.store.InstalledPluginRef;
|
||||
import pitiupi.plugins.store.PluginStore;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* CLI mínima para smoke REQ-02 (instalar/listar/remover JAR) sem Swing.
|
||||
*/
|
||||
public final class PluginStoreCli {
|
||||
|
||||
private PluginStoreCli() {
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
if (args.length < 1) {
|
||||
usage();
|
||||
System.exit(1);
|
||||
}
|
||||
try {
|
||||
PitiupiHome.get().ensureLayout();
|
||||
PluginStore store = new PluginStore();
|
||||
switch (args[0]) {
|
||||
case "install" -> install(store, args);
|
||||
case "list" -> list(store);
|
||||
case "remove" -> remove(store, args);
|
||||
default -> {
|
||||
usage();
|
||||
System.exit(1);
|
||||
}
|
||||
}
|
||||
} catch (PitiupiException ex) {
|
||||
System.err.println("ERRO: " + ex.getUserMessage());
|
||||
System.exit(2);
|
||||
} catch (Exception ex) {
|
||||
System.err.println("ERRO: " + ex.getMessage());
|
||||
System.exit(2);
|
||||
}
|
||||
}
|
||||
|
||||
private static void install(PluginStore store, String[] args) throws Exception {
|
||||
if (args.length < 2) {
|
||||
usage();
|
||||
System.exit(1);
|
||||
}
|
||||
Path dest = store.installFromFile(Path.of(args[1]));
|
||||
System.out.println("OK install " + dest.getFileName());
|
||||
}
|
||||
|
||||
private static void list(PluginStore store) throws Exception {
|
||||
List<InstalledPluginRef> refs = store.listInstalled();
|
||||
if (refs.isEmpty()) {
|
||||
System.out.println("(vazio)");
|
||||
return;
|
||||
}
|
||||
for (InstalledPluginRef ref : refs) {
|
||||
System.out.println(ref.getFileName() + "\t" + ref.getJarPath());
|
||||
}
|
||||
}
|
||||
|
||||
private static void remove(PluginStore store, String[] args) throws Exception {
|
||||
if (args.length < 2) {
|
||||
usage();
|
||||
System.exit(1);
|
||||
}
|
||||
Path jar = Path.of(args[1]);
|
||||
store.remove(jar);
|
||||
System.out.println("OK remove " + jar.getFileName());
|
||||
}
|
||||
|
||||
private static void usage() {
|
||||
System.err.println("Uso: PluginStoreCli install <arquivo.jar> | list | remove <caminho-jar-em-plugins>");
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
/dist/
|
||||
/build/
|
||||
/out/
|
||||
/nbproject/private/
|
||||
/nbproject/
|
||||
/src/
|
||||
/build.xml
|
||||
*.jar
|
||||
@@ -1,6 +1,6 @@
|
||||
# Plugin Ping (exercício de aula)
|
||||
|
||||
Material didático para o aluno **implementar** um plugin de Ping/Pong com medição de RTT. Este diretório no repositório oficial contém apenas planejamento e referências — **não** inclua código Java ou JAR da solução no commit.
|
||||
Material didático para o aluno **implementar** um plugin de Ping/Pong com medição de RTT. No Git ficam só este README e o [tutorial](../../docs/documentation/17_tutorial_ping_aula.md). Código e `build.xml` locais estão no `.gitignore` (`src/`, `nbproject/`, `dist/`).
|
||||
|
||||
## Onde está o tutorial
|
||||
|
||||
@@ -14,8 +14,6 @@ Tutorial canônico (passo a passo): [docs/documentation/17_tutorial_ping_aula.md
|
||||
4. Instalar em `PITIUPI_HOME/plugins/` e reiniciar o Pitiupi.
|
||||
5. Abrir o plugin pela aba **Plugins** na galeria (aba não abre sozinha no boot).
|
||||
|
||||
Quando o JAR estiver pronto para o time, registre uma entrada em [`plugins.registry.json`](../../plugins.registry.json) na raiz do clone (id `ping`, `dir`: `plugins/ping`, etc.).
|
||||
|
||||
## Pré-requisitos
|
||||
|
||||
- Núcleo compilado: `../../pitiupi/dist/pitiupi.jar`
|
||||
|
||||
Reference in new issue
Block a user