Compare commits
124 Commits
muwire-0.5
...
muwire-0.6
Author | SHA1 | Date | |
---|---|---|---|
![]() |
0c40c8f269 | ||
![]() |
681ddb99a2 | ||
![]() |
5dff319746 | ||
![]() |
57c4a00ac6 | ||
![]() |
286a0a8678 | ||
![]() |
17eff7d77f | ||
![]() |
2e22369ce0 | ||
![]() |
15c59b440f | ||
![]() |
8fb015acbf | ||
![]() |
f7b11c90fd | ||
![]() |
df93a35062 | ||
![]() |
ecb19a8412 | ||
![]() |
b1e5b40800 | ||
![]() |
daa3a293f2 | ||
![]() |
907264fc67 | ||
![]() |
c6becb93dc | ||
![]() |
2954bd2f1a | ||
![]() |
35322d2c15 | ||
![]() |
9f6a7eb368 | ||
![]() |
fec81808e5 | ||
![]() |
4db890484d | ||
![]() |
dfd5e06889 | ||
![]() |
71da8e14da | ||
![]() |
7dc37e3e0d | ||
![]() |
3de058a078 | ||
![]() |
4d70c7adce | ||
![]() |
5b41106476 | ||
![]() |
6240b22e66 | ||
![]() |
0e26f5afd7 | ||
![]() |
114bc06dbb | ||
![]() |
5fa2f2753c | ||
![]() |
cacdd2a7a9 | ||
![]() |
d56f7c6184 | ||
![]() |
f7f4513109 | ||
![]() |
dd15d893ba | ||
![]() |
bf5ab9c82e | ||
![]() |
edd5a29b10 | ||
![]() |
38eb89f2f7 | ||
![]() |
73f1d64428 | ||
![]() |
bc1cae2d75 | ||
![]() |
a0ab07a7c0 | ||
![]() |
f875c379ce | ||
![]() |
0ce9784ccf | ||
![]() |
be82136e32 | ||
![]() |
7d25bb9364 | ||
![]() |
c6e98db9d4 | ||
![]() |
35a26e2a47 | ||
![]() |
beef4af329 | ||
![]() |
cec3c1bc0f | ||
![]() |
289b958784 | ||
![]() |
e9c554d717 | ||
![]() |
1875fcddb2 | ||
![]() |
bee6154fa9 | ||
![]() |
1f9b171021 | ||
![]() |
59c03be35e | ||
![]() |
621af96bdf | ||
![]() |
bcb7016202 | ||
![]() |
b1b2bcaef8 | ||
![]() |
eec007e83b | ||
![]() |
3d36351a6b | ||
![]() |
d57d2ccb71 | ||
![]() |
d91f15ee54 | ||
![]() |
6bc61c920d | ||
![]() |
146ed53e12 | ||
![]() |
8ebae1600b | ||
![]() |
18d19ca75e | ||
![]() |
29e499fe9d | ||
![]() |
3db167bade | ||
![]() |
bfe0ab7867 | ||
![]() |
1fbb1e7932 | ||
![]() |
0632336cd1 | ||
![]() |
aa221cd6dc | ||
![]() |
29b5c55328 | ||
![]() |
5e7f3587df | ||
![]() |
8afd387ca6 | ||
![]() |
5d16963d1c | ||
![]() |
6080c8b308 | ||
![]() |
915deb1dee | ||
![]() |
8afca3dc7f | ||
![]() |
f072d0343c | ||
![]() |
a549ad3d8d | ||
![]() |
b6f5ec7d22 | ||
![]() |
761bf0a177 | ||
![]() |
bd873211c0 | ||
![]() |
036971cfe5 | ||
![]() |
a2637570b1 | ||
![]() |
6012adbeab | ||
![]() |
8f6b6b0caa | ||
![]() |
8f3b5aea8d | ||
![]() |
ee098ace8e | ||
![]() |
5d8401e4bf | ||
![]() |
fbf9add82a | ||
![]() |
7379263fef | ||
![]() |
7d50843754 | ||
![]() |
f4a2864942 | ||
![]() |
afaadf65a4 | ||
![]() |
7bd422d6b4 | ||
![]() |
3f47274f61 | ||
![]() |
419e9a0ce6 | ||
![]() |
ac1068a681 | ||
![]() |
549457e36f | ||
![]() |
14d6d10546 | ||
![]() |
878e397aa0 | ||
![]() |
27831b488b | ||
![]() |
449f46c62b | ||
![]() |
5703b85386 | ||
![]() |
76d8d847bd | ||
![]() |
db84d8e5bf | ||
![]() |
cc9b384907 | ||
![]() |
72960c24a8 | ||
![]() |
71298e5e73 | ||
![]() |
11bc672544 | ||
![]() |
2f6cd311a0 | ||
![]() |
0448750491 | ||
![]() |
800dd1cbba | ||
![]() |
f95e9450f3 | ||
![]() |
d842e3f2f2 | ||
![]() |
2017b53a43 | ||
![]() |
6e2b3f4f33 | ||
![]() |
dbb305139b | ||
![]() |
0801bfec08 | ||
![]() |
00a8d100fe | ||
![]() |
e94b7cb0d4 | ||
![]() |
b0357f2ecd |
12
README.md
12
README.md
@@ -4,11 +4,11 @@ MuWire is an easy to use file-sharing program which offers anonymity using [I2P
|
||||
|
||||
It is inspired by the LimeWire Gnutella client and developped by a former LimeWire developer.
|
||||
|
||||
The current stable release - 0.5.5 is avaiable for download at https://muwire.com. You can find technical documentation in the "doc" folder.
|
||||
The current stable release - 0.6.5 is avaiable for download at https://muwire.com. You can find technical documentation in the "doc" folder.
|
||||
|
||||
### Building
|
||||
|
||||
You need JDK 8 or newer. After installing that and setting up the appropriate paths, just type
|
||||
You need JDK 9 or newer. After installing that and setting up the appropriate paths, just type
|
||||
|
||||
```
|
||||
./gradlew clean assemble
|
||||
@@ -23,7 +23,7 @@ If you want to build binary bundles that do not depend on Java or I2P, see the h
|
||||
|
||||
### Running the GUI
|
||||
|
||||
After you build the application, look inside `gui/build/distributions`. Untar/unzip one of the `shadow` files and then run the jar contained inside by typing `java -jar gui-x.y.z.jar` in a terminal or command prompt.
|
||||
After you build the application, look inside `gui/build/distributions`. Untar/unzip one of the `shadow` files and then run the jar contained inside by typing `java -jar gui-x.y.z-all.jar` in a terminal or command prompt.
|
||||
|
||||
If you have an I2P router running on the same machine that is all you need to do. If you use a custom I2CP host and port, create a file `i2p.properties` and put `i2cp.tcp.host=<host>` and `i2cp.tcp.port=<port>` in there. On Windows that file should go into `%HOME%\AppData\Roaming\MuWire`, on Mac into `$HOME/Library/Application Support/MuWire` and on Linux `$HOME/.MuWire`
|
||||
|
||||
@@ -31,10 +31,14 @@ If you have an I2P router running on the same machine that is all you need to do
|
||||
|
||||
### Running the CLI
|
||||
|
||||
Look inside `cli-lanterna/build/distributions`. Untar/unzip one of the `shadow` files and then run the jar contained inside by typing `java -jar cli-lanterna-x.y.z.jar` in a terminal. The CLI will ask you about the router host and port on startup, no need to edit any files. However, the CLI does not have an options window yet, so if you need to change any options you will need to edit the configuration files. The CLI options are documented here https://github.com/zlatinb/muwire/wiki/CLI-Configuration-Options
|
||||
Look inside `cli-lanterna/build/distributions`. Untar/unzip one of the `shadow` files and then run the jar contained inside by typing `java -jar cli-lanterna-x.y.z-all.jar` in a terminal. The CLI will ask you about the router host and port on startup, no need to edit any files. However, the CLI does not have an options window yet, so if you need to change any options you will need to edit the configuration files. The CLI options are documented here https://github.com/zlatinb/muwire/wiki/CLI-Configuration-Options
|
||||
|
||||
The CLI is under active development and doesn't have all the features of the GUI.
|
||||
|
||||
### Web UI
|
||||
|
||||
If you are a Grails/Scala/JRuby/Kotlin developer and are interested in building a Web UI for MuWire, please get in touch. The MuWire core is written in Groovy and should be easy to integrate with any JVM-based language.
|
||||
|
||||
### GPG Fingerprint
|
||||
|
||||
```
|
||||
|
@@ -6,7 +6,7 @@ buildscript {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.github.jengelman.gradle.plugins:shadow:2.0.4'
|
||||
classpath 'com.github.jengelman.gradle.plugins:shadow:5.2.0'
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -72,4 +72,27 @@ class BrowseModel {
|
||||
void setPercentageLabel(Label percentage) {
|
||||
this.percentage = percentage
|
||||
}
|
||||
|
||||
void sort(SortType type) {
|
||||
Comparator<UIResultEvent> chosen
|
||||
switch(type) {
|
||||
case SortType.NAME_ASC : chosen = ResultComparators.NAME_ASC; break
|
||||
case SortType.NAME_DESC : chosen = ResultComparators.NAME_DESC; break
|
||||
case SortType.SIZE_ASC : chosen = ResultComparators.SIZE_ASC; break
|
||||
case SortType.SIZE_DESC : chosen = ResultComparators.SIZE_DESC; break
|
||||
}
|
||||
|
||||
List<UIResultEvent> l = new ArrayList<>(rootToResult.values())
|
||||
Collections.sort(l, chosen)
|
||||
|
||||
int rowCount = model.getRowCount()
|
||||
rowCount.times { model.removeRow(0) }
|
||||
|
||||
l.each { e ->
|
||||
String size = DataHelper.formatSize2Decimal(e.size, false) + "B"
|
||||
String infoHash = Base64.encode(e.infohash.getRoot())
|
||||
String comment = String.valueOf(e.comment != null)
|
||||
model.addRow(e.name, size, infoHash, comment, e.certificates)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -58,11 +58,17 @@ class BrowseView extends BasicWindow {
|
||||
}
|
||||
contentPanel.addComponent(table, layoutData)
|
||||
|
||||
Panel buttonsPanel = new Panel()
|
||||
buttonsPanel.setLayoutManager(new GridLayout(2))
|
||||
Button sortButton = new Button("Sort...", {sort()})
|
||||
Button closeButton = new Button("Close",{
|
||||
model.unregister()
|
||||
close()
|
||||
})
|
||||
contentPanel.addComponent(closeButton, layoutData)
|
||||
buttonsPanel.addComponent(sortButton, layoutData)
|
||||
buttonsPanel.addComponent(closeButton, layoutData)
|
||||
|
||||
contentPanel.addComponent(buttonsPanel, layoutData)
|
||||
setComponent(contentPanel)
|
||||
|
||||
}
|
||||
@@ -120,4 +126,11 @@ class BrowseView extends BasicWindow {
|
||||
ViewCertificatesView view = new ViewCertificatesView(model, textGUI, core, terminalSize)
|
||||
textGUI.addWindowAndWait(view)
|
||||
}
|
||||
|
||||
private void sort() {
|
||||
SortPrompt prompt = new SortPrompt(textGUI)
|
||||
SortType type = prompt.prompt()
|
||||
if (type != null)
|
||||
model.sort(type)
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,88 @@
|
||||
package com.muwire.clilanterna
|
||||
|
||||
import com.googlecode.lanterna.gui2.TextBox
|
||||
import com.googlecode.lanterna.gui2.TextGUIThread
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.chat.ChatConnectionEvent
|
||||
import com.muwire.core.chat.ChatLink
|
||||
import com.muwire.core.chat.ChatMessageEvent
|
||||
import com.muwire.core.chat.UIConnectChatEvent
|
||||
|
||||
import net.i2p.data.DataHelper
|
||||
|
||||
class ChatConsoleModel {
|
||||
private final Core core
|
||||
private final TextGUIThread guiThread
|
||||
|
||||
volatile ChatLink link
|
||||
volatile Thread poller
|
||||
volatile boolean running
|
||||
|
||||
volatile TextBox textBox
|
||||
|
||||
|
||||
ChatConsoleModel(Core core, TextGUIThread guiThread) {
|
||||
this.core = core
|
||||
this.guiThread = guiThread
|
||||
}
|
||||
|
||||
void start() {
|
||||
if (running)
|
||||
return
|
||||
running = true
|
||||
core.chatServer.start()
|
||||
core.eventBus.with {
|
||||
register(ChatConnectionEvent.class, this)
|
||||
publish(new UIConnectChatEvent(host : core.me))
|
||||
}
|
||||
}
|
||||
|
||||
void onChatConnectionEvent(ChatConnectionEvent e) {
|
||||
if (e.persona != core.me)
|
||||
return // can't really happen
|
||||
|
||||
link = e.connection
|
||||
poller = new Thread({eventLoop()} as Runnable)
|
||||
poller.setDaemon(true)
|
||||
poller.start()
|
||||
}
|
||||
|
||||
void stop() {
|
||||
if (!running)
|
||||
return
|
||||
running = false
|
||||
core.chatServer.stop()
|
||||
poller?.interrupt()
|
||||
link = null
|
||||
}
|
||||
|
||||
private void eventLoop() {
|
||||
Thread.sleep(1000)
|
||||
while(running) {
|
||||
ChatLink link = this.link
|
||||
if (link == null || !link.isUp()) {
|
||||
Thread.sleep(100)
|
||||
continue
|
||||
}
|
||||
|
||||
Object event = link.nextEvent()
|
||||
if (event instanceof ChatMessageEvent)
|
||||
handleChatMessage(event)
|
||||
else if (event instanceof Persona)
|
||||
handleLeave(event)
|
||||
else
|
||||
throw new IllegalArgumentException("unknown event type $event")
|
||||
}
|
||||
}
|
||||
|
||||
private void handleChatMessage(ChatMessageEvent e) {
|
||||
String text = DataHelper.formatTime(e.timestamp)+" <"+e.sender.getHumanReadableName()+ "> ["+
|
||||
e.room+"] "+e.payload
|
||||
guiThread.invokeLater({textBox.addLine(text)})
|
||||
}
|
||||
|
||||
private void handleLeave(Persona p) {
|
||||
guiThread.invokeLater({textBox.addLine(p.getHumanReadableName()+ " disconnected")})
|
||||
}
|
||||
}
|
@@ -0,0 +1,101 @@
|
||||
package com.muwire.clilanterna
|
||||
|
||||
import com.googlecode.lanterna.TerminalSize
|
||||
import com.googlecode.lanterna.gui2.BasicWindow
|
||||
import com.googlecode.lanterna.gui2.Button
|
||||
import com.googlecode.lanterna.gui2.GridLayout
|
||||
import com.googlecode.lanterna.gui2.GridLayout.Alignment
|
||||
import com.googlecode.lanterna.gui2.Label
|
||||
import com.googlecode.lanterna.gui2.LayoutData
|
||||
import com.googlecode.lanterna.gui2.Panel
|
||||
import com.googlecode.lanterna.gui2.TextBox
|
||||
import com.googlecode.lanterna.gui2.TextGUI
|
||||
import com.googlecode.lanterna.gui2.Window
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.chat.ChatConnection
|
||||
import com.muwire.core.chat.ChatMessageEvent
|
||||
import com.muwire.core.chat.ChatServer
|
||||
|
||||
import net.i2p.data.DataHelper
|
||||
|
||||
class ChatConsoleView extends BasicWindow {
|
||||
private final TextGUI textGUI
|
||||
private final ChatConsoleModel model
|
||||
private final Core core
|
||||
|
||||
private final LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER, true, false)
|
||||
|
||||
private final TextBox textBox
|
||||
private final TextBox sayField
|
||||
private final TextBox roomField
|
||||
|
||||
ChatConsoleView(Core core, ChatConsoleModel model, TextGUI textGUI, TerminalSize terminalSize) {
|
||||
super("Chat Server Console")
|
||||
this.core = core
|
||||
this.model = model
|
||||
this.textGUI = textGUI
|
||||
this.textBox = new TextBox(terminalSize,"", TextBox.Style.MULTI_LINE)
|
||||
model.textBox = textBox
|
||||
model.start()
|
||||
this.sayField = new TextBox("", TextBox.Style.SINGLE_LINE)
|
||||
this.roomField = new TextBox("__CONSOLE__", TextBox.Style.SINGLE_LINE)
|
||||
|
||||
|
||||
setHints([Window.Hint.EXPANDED])
|
||||
|
||||
Panel contentPanel = new Panel()
|
||||
contentPanel.setLayoutManager(new GridLayout(1))
|
||||
contentPanel.addComponent(textBox, layoutData)
|
||||
|
||||
Panel inputPanel = new Panel()
|
||||
inputPanel.with {
|
||||
setLayoutManager(new GridLayout(2))
|
||||
addComponent(new Label("Say something here"), layoutData)
|
||||
addComponent(sayField, layoutData)
|
||||
addComponent(new Label("In room:"), layoutData)
|
||||
addComponent(roomField, layoutData)
|
||||
}
|
||||
contentPanel.addComponent(inputPanel, layoutData)
|
||||
|
||||
Panel bottomPanel = new Panel()
|
||||
bottomPanel.setLayoutManager(new GridLayout(4))
|
||||
|
||||
Button sayButton = new Button("Say",{say()})
|
||||
Button startButton = new Button("Start Server",{model.start()})
|
||||
Button stopButton = new Button("Stop Server", {model.stop()})
|
||||
Button closeButton = new Button("Close",{close()})
|
||||
|
||||
bottomPanel.with {
|
||||
addComponent(sayButton, layoutData)
|
||||
addComponent(startButton, layoutData)
|
||||
addComponent(stopButton, layoutData)
|
||||
addComponent(closeButton, layoutData)
|
||||
}
|
||||
contentPanel.addComponent(bottomPanel, layoutData)
|
||||
setComponent(contentPanel)
|
||||
}
|
||||
|
||||
private void say() {
|
||||
String command = sayField.getText()
|
||||
sayField.setText("")
|
||||
String room = roomField.getText()
|
||||
|
||||
UUID uuid = UUID.randomUUID()
|
||||
long now = System.currentTimeMillis()
|
||||
|
||||
String toAppend = DataHelper.formatTime(now) + " <" + core.me.getHumanReadableName() + "> [$room] " + command
|
||||
textBox.addLine(toAppend)
|
||||
|
||||
byte[] sig = ChatConnection.sign(uuid, now, room, command, core.me, core.me, core.spk)
|
||||
|
||||
def event = new ChatMessageEvent( uuid : uuid,
|
||||
payload : command,
|
||||
sender : core.me,
|
||||
host : core.me,
|
||||
room : room,
|
||||
chatTime : now,
|
||||
sig : sig
|
||||
)
|
||||
core.eventBus.publish(event)
|
||||
}
|
||||
}
|
@@ -32,7 +32,7 @@ import com.muwire.core.UILoadedEvent
|
||||
import com.muwire.core.files.AllFilesLoadedEvent
|
||||
|
||||
class CliLanterna {
|
||||
private static final String MW_VERSION = "0.5.9"
|
||||
private static final String MW_VERSION = "0.6.6"
|
||||
|
||||
private static volatile Core core
|
||||
|
||||
|
@@ -78,4 +78,32 @@ class FilesModel {
|
||||
model.addRow(new SharedFileWrapper(it), DataHelper.formatSize2(size, false)+"B", comment, certified, hits, downloaders)
|
||||
}
|
||||
}
|
||||
|
||||
private void sort(SortType type) {
|
||||
Comparator<SharedFile> chosen
|
||||
switch(type) {
|
||||
case SortType.NAME_ASC : chosen = NAME_ASC; break
|
||||
case SortType.NAME_DESC : chosen = NAME_DESC; break
|
||||
case SortType.SIZE_ASC : chosen = SIZE_ASC; break
|
||||
case SortType.SIZE_DESC : chosen = SIZE_DESC; break
|
||||
}
|
||||
|
||||
Collections.sort(sharedFiles, chosen)
|
||||
}
|
||||
|
||||
private static final Comparator<SharedFile> NAME_ASC = new Comparator<SharedFile>() {
|
||||
public int compare(SharedFile a, SharedFile b) {
|
||||
a.getFile().getName().compareTo(b.getFile().getName())
|
||||
}
|
||||
}
|
||||
|
||||
private static final Comparator<SharedFile> NAME_DESC = NAME_ASC.reversed()
|
||||
|
||||
private static final Comparator<SharedFile> SIZE_ASC = new Comparator<SharedFile>() {
|
||||
public int compare(SharedFile a, SharedFile b) {
|
||||
Long.compare(a.getCachedLength(), b.getCachedLength())
|
||||
}
|
||||
}
|
||||
|
||||
private static final Comparator<SharedFile> SIZE_DESC = SIZE_ASC.reversed()
|
||||
}
|
||||
|
@@ -51,17 +51,19 @@ class FilesView extends BasicWindow {
|
||||
contentPanel.addComponent(table, layoutData)
|
||||
|
||||
Panel buttonsPanel = new Panel()
|
||||
buttonsPanel.setLayoutManager(new GridLayout(4))
|
||||
buttonsPanel.setLayoutManager(new GridLayout(5))
|
||||
|
||||
Button shareFile = new Button("Share File", {shareFile()})
|
||||
Button shareDirectory = new Button("Share Directory", {shareDirectory()})
|
||||
Button unshareDirectory = new Button("Unshare Directory",{unshareDirectory()})
|
||||
Button sort = new Button("Sort...",{sort()})
|
||||
Button close = new Button("Close", {close()})
|
||||
|
||||
buttonsPanel.with {
|
||||
addComponent(shareFile, layoutData)
|
||||
addComponent(shareDirectory, layoutData)
|
||||
addComponent(unshareDirectory, layoutData)
|
||||
addComponent(sort, layoutData)
|
||||
addComponent(close, layoutData)
|
||||
}
|
||||
|
||||
@@ -134,4 +136,11 @@ class FilesView extends BasicWindow {
|
||||
core.eventBus.publish(new DirectoryUnsharedEvent(directory : directory))
|
||||
MessageDialog.showMessageDialog(textGUI, "Directory Unshared", directory.getName()+" has been unshared", MessageDialogButton.OK)
|
||||
}
|
||||
|
||||
private void sort() {
|
||||
SortPrompt prompt = new SortPrompt(textGUI)
|
||||
SortType type = prompt.prompt()
|
||||
if (type != null)
|
||||
model.sort(type)
|
||||
}
|
||||
}
|
||||
|
@@ -44,6 +44,7 @@ class MainWindowView extends BasicWindow {
|
||||
private final UploadsModel uploadsModel
|
||||
private final FilesModel filesModel
|
||||
private final TrustModel trustModel
|
||||
private final ChatConsoleModel chatModel
|
||||
|
||||
private final Label connectionCount, incoming, outgoing
|
||||
private final Label known, failing, hopeless
|
||||
@@ -63,6 +64,9 @@ class MainWindowView extends BasicWindow {
|
||||
uploadsModel = new UploadsModel(textGUI.getGUIThread(), core, props)
|
||||
filesModel = new FilesModel(textGUI.getGUIThread(),core)
|
||||
trustModel = new TrustModel(textGUI.getGUIThread(), core)
|
||||
chatModel = new ChatConsoleModel(core, textGUI.getGUIThread())
|
||||
if (core.muOptions.startChatServer)
|
||||
core.chatServer.start()
|
||||
|
||||
setHints([Window.Hint.EXPANDED])
|
||||
Panel contentPanel = new Panel()
|
||||
@@ -74,7 +78,7 @@ class MainWindowView extends BasicWindow {
|
||||
Panel buttonsPanel = new Panel()
|
||||
contentPanel.addComponent(buttonsPanel, BorderLayout.Location.TOP)
|
||||
|
||||
GridLayout gridLayout = new GridLayout(7)
|
||||
GridLayout gridLayout = new GridLayout(8)
|
||||
buttonsPanel.setLayoutManager(gridLayout)
|
||||
|
||||
searchTextBox = new TextBox(new TerminalSize(40, 1))
|
||||
@@ -83,6 +87,7 @@ class MainWindowView extends BasicWindow {
|
||||
Button uploadsButton = new Button("Uploads", {upload()})
|
||||
Button filesButton = new Button("Files", { files() })
|
||||
Button trustButton = new Button("Trust", {trust()})
|
||||
Button chatButton = new Button("Chat", {chat()})
|
||||
Button quitButton = new Button("Quit", {close()})
|
||||
|
||||
LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER)
|
||||
@@ -94,6 +99,7 @@ class MainWindowView extends BasicWindow {
|
||||
addComponent(uploadsButton, layoutData)
|
||||
addComponent(filesButton, layoutData)
|
||||
addComponent(trustButton, layoutData)
|
||||
addComponent(chatButton, layoutData)
|
||||
addComponent(quitButton, layoutData)
|
||||
}
|
||||
|
||||
@@ -271,6 +277,10 @@ class MainWindowView extends BasicWindow {
|
||||
textGUI.addWindowAndWait(new TrustView(trustModel, textGUI, core, sizeForTables()))
|
||||
}
|
||||
|
||||
private void chat() {
|
||||
textGUI.addWindowAndWait(new ChatConsoleView(core, chatModel, textGUI, sizeForTables()))
|
||||
}
|
||||
|
||||
private void refreshStats() {
|
||||
int inCon = 0
|
||||
int outCon = 0
|
||||
|
@@ -0,0 +1,21 @@
|
||||
package com.muwire.clilanterna
|
||||
|
||||
import com.muwire.core.search.UIResultEvent
|
||||
|
||||
class ResultComparators {
|
||||
public static final Comparator<UIResultEvent> NAME_ASC = new Comparator<UIResultEvent>() {
|
||||
public int compare(UIResultEvent a, UIResultEvent b) {
|
||||
a.name.compareTo(b.name)
|
||||
}
|
||||
}
|
||||
|
||||
public static final Comparator<UIResultEvent> NAME_DESC = NAME_ASC.reversed()
|
||||
|
||||
public static final Comparator<UIResultEvent> SIZE_ASC = new Comparator<UIResultEvent>() {
|
||||
public int compare(UIResultEvent a, UIResultEvent b) {
|
||||
Long.compare(a.size, b.size)
|
||||
}
|
||||
}
|
||||
|
||||
public static final Comparator<UIResultEvent> SIZE_DESC = SIZE_ASC.reversed()
|
||||
}
|
@@ -16,7 +16,27 @@ class ResultsModel {
|
||||
ResultsModel(UIResultBatchEvent results) {
|
||||
this.results = results
|
||||
model = new TableModel("Name","Size","Hash","Sources","Comment","Certificates")
|
||||
results.results.each {
|
||||
updateModel()
|
||||
}
|
||||
|
||||
void sort(SortType type) {
|
||||
Comparator<UIResultEvent> chosen
|
||||
switch(type) {
|
||||
case SortType.NAME_ASC : chosen = ResultComparators.NAME_ASC; break
|
||||
case SortType.NAME_DESC : chosen = ResultComparators.NAME_DESC; break
|
||||
case SortType.SIZE_ASC : chosen = ResultComparators.SIZE_ASC; break
|
||||
case SortType.SIZE_DESC : chosen = ResultComparators.SIZE_DESC; break
|
||||
}
|
||||
|
||||
Arrays.sort(results.results, chosen)
|
||||
updateModel()
|
||||
}
|
||||
|
||||
private void updateModel() {
|
||||
int rowCount = model.getRowCount()
|
||||
rowCount.times { model.removeRow(0) }
|
||||
|
||||
results.results.each {
|
||||
String size = DataHelper.formatSize2Decimal(it.size, false) + "B"
|
||||
String infoHash = Base64.encode(it.infohash.getRoot())
|
||||
String sources = String.valueOf(it.sources.size())
|
||||
|
@@ -43,9 +43,14 @@ class ResultsView extends BasicWindow {
|
||||
table.setTableModel(model.model)
|
||||
table.setVisibleRows(terminalSize.getRows())
|
||||
contentPanel.addComponent(table, GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER, true, false))
|
||||
|
||||
|
||||
Panel buttonsPanel = new Panel()
|
||||
buttonsPanel.setLayoutManager(new GridLayout(2))
|
||||
Button sortButton = new Button("Sort...",{sort()})
|
||||
buttonsPanel.addComponent(sortButton)
|
||||
Button closeButton = new Button("Close", {close()})
|
||||
contentPanel.addComponent(closeButton, GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER, true, false))
|
||||
buttonsPanel.addComponent(closeButton)
|
||||
contentPanel.addComponent(buttonsPanel, GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER, true, false))
|
||||
|
||||
setComponent(contentPanel)
|
||||
closeButton.takeFocus()
|
||||
@@ -109,4 +114,11 @@ class ResultsView extends BasicWindow {
|
||||
ViewCertificatesView view = new ViewCertificatesView(model, textGUI, core, terminalSize)
|
||||
textGUI.addWindowAndWait(view)
|
||||
}
|
||||
|
||||
private void sort() {
|
||||
SortPrompt prompt = new SortPrompt(textGUI)
|
||||
SortType type = prompt.prompt()
|
||||
if (type != null)
|
||||
model.sort(type)
|
||||
}
|
||||
}
|
||||
|
@@ -7,6 +7,7 @@ import com.muwire.core.search.QueryEvent
|
||||
import com.muwire.core.search.SearchEvent
|
||||
import com.muwire.core.search.UIResultBatchEvent
|
||||
import com.muwire.core.search.UIResultEvent
|
||||
import com.muwire.core.util.DataUtil
|
||||
|
||||
import net.i2p.crypto.DSAEngine
|
||||
import net.i2p.data.Base64
|
||||
@@ -45,13 +46,16 @@ class SearchModel {
|
||||
|
||||
def searchEvent
|
||||
byte [] payload
|
||||
UUID uuid = UUID.randomUUID()
|
||||
long timestamp = System.currentTimeMillis()
|
||||
byte [] sig2 = DataUtil.signUUID(uuid, timestamp, core.spk)
|
||||
if (hashSearch) {
|
||||
searchEvent = new SearchEvent(searchHash : root, uuid : UUID.randomUUID(), oobInfohash : true, compressedResults : true)
|
||||
searchEvent = new SearchEvent(searchHash : root, uuid : uuid, oobInfohash : true, compressedResults : true)
|
||||
payload = root
|
||||
} else {
|
||||
def nonEmpty = SplitPattern.termify(query)
|
||||
payload = String.join(" ", nonEmpty).getBytes(StandardCharsets.UTF_8)
|
||||
searchEvent = new SearchEvent(searchTerms : nonEmpty, uuid : UUID.randomUUID(), oobInfohash: true,
|
||||
searchEvent = new SearchEvent(searchTerms : nonEmpty, uuid : uuid, oobInfohash: true,
|
||||
searchComments : core.muOptions.searchComments, compressedResults : true)
|
||||
}
|
||||
|
||||
@@ -61,7 +65,7 @@ class SearchModel {
|
||||
|
||||
core.eventBus.publish(new QueryEvent(searchEvent : searchEvent, firstHop : firstHop,
|
||||
replyTo: core.me.destination, receivedOn: core.me.destination,
|
||||
originator : core.me, sig: sig.data))
|
||||
originator : core.me, sig: sig.data, queryTime : timestamp, sig2 : sig2))
|
||||
}
|
||||
|
||||
void unregister() {
|
||||
|
@@ -0,0 +1,57 @@
|
||||
package com.muwire.clilanterna
|
||||
|
||||
import com.googlecode.lanterna.gui2.BasicWindow
|
||||
import com.googlecode.lanterna.gui2.Button
|
||||
import com.googlecode.lanterna.gui2.GridLayout
|
||||
import com.googlecode.lanterna.gui2.GridLayout.Alignment
|
||||
import com.googlecode.lanterna.gui2.LayoutData
|
||||
import com.googlecode.lanterna.gui2.Panel
|
||||
import com.googlecode.lanterna.gui2.TextGUI
|
||||
import com.googlecode.lanterna.gui2.Window
|
||||
|
||||
class SortPrompt extends BasicWindow {
|
||||
private final TextGUI textGUI
|
||||
private SortType type
|
||||
SortPrompt(TextGUI textGUI) {
|
||||
super("Select what to sort by")
|
||||
this.textGUI = textGUI
|
||||
}
|
||||
|
||||
SortType prompt() {
|
||||
setHints([Window.Hint.CENTERED])
|
||||
Panel contentPanel = new Panel()
|
||||
contentPanel.setLayoutManager(new GridLayout(5))
|
||||
|
||||
LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER)
|
||||
|
||||
Button nameAsc = new Button("Name (ascending)",{
|
||||
type = SortType.NAME_ASC
|
||||
close()
|
||||
})
|
||||
Button nameDesc = new Button("Name (descending)",{
|
||||
type = SortType.NAME_DESC
|
||||
close()
|
||||
})
|
||||
Button sizeAsc = new Button("Size (ascending)",{
|
||||
type = SortType.SIZE_ASC
|
||||
close()
|
||||
})
|
||||
Button sizeDesc = new Button("Size (descending)",{
|
||||
type = SortType.SIZE_DESC
|
||||
close()
|
||||
})
|
||||
Button close = new Button("Cancel",{close()})
|
||||
|
||||
contentPanel.with {
|
||||
addComponent(nameAsc, layoutData)
|
||||
addComponent(nameDesc, layoutData)
|
||||
addComponent(sizeAsc, layoutData)
|
||||
addComponent(sizeDesc, layoutData)
|
||||
addComponent(close, layoutData)
|
||||
}
|
||||
|
||||
setComponent(contentPanel)
|
||||
textGUI.addWindowAndWait(this)
|
||||
type
|
||||
}
|
||||
}
|
@@ -0,0 +1,5 @@
|
||||
package com.muwire.clilanterna;
|
||||
|
||||
public enum SortType {
|
||||
NAME_ASC,NAME_DESC,SIZE_ASC,SIZE_DESC
|
||||
}
|
@@ -0,0 +1,7 @@
|
||||
package com.muwire.clilanterna
|
||||
|
||||
import com.muwire.core.trust.TrustService
|
||||
|
||||
class TrustEntryWrapper {
|
||||
TrustService.TrustEntry entry
|
||||
}
|
@@ -16,8 +16,8 @@ class TrustListModel {
|
||||
this.trustList = trustList
|
||||
this.core = core
|
||||
|
||||
trustedTableModel = new TableModel("Trusted User","Your Trust")
|
||||
distrustedTableModel = new TableModel("Distrusted User", "Your Trust")
|
||||
trustedTableModel = new TableModel("Trusted User","Reason","Your Trust")
|
||||
distrustedTableModel = new TableModel("Distrusted User", "Reason", "Your Trust")
|
||||
refreshModels()
|
||||
|
||||
core.eventBus.register(TrustEvent.class, this)
|
||||
@@ -36,10 +36,10 @@ class TrustListModel {
|
||||
distrustRows.times { distrustedTableModel.removeRow(0) }
|
||||
|
||||
trustList.good.each {
|
||||
trustedTableModel.addRow(new PersonaWrapper(it), core.trustService.getLevel(it.destination))
|
||||
trustedTableModel.addRow(new PersonaWrapper(it.persona),it.reason, core.trustService.getLevel(it.persona.destination))
|
||||
}
|
||||
trustList.bad.each {
|
||||
distrustedTableModel.addRow(new PersonaWrapper(it), core.trustService.getLevel(it.destination))
|
||||
distrustedTableModel.addRow(new PersonaWrapper(it.persona),it.reason, core.trustService.getLevel(it.persona.destination))
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -12,6 +12,7 @@ import com.googlecode.lanterna.gui2.TextGUI
|
||||
import com.googlecode.lanterna.gui2.Window
|
||||
import com.googlecode.lanterna.gui2.dialogs.MessageDialog
|
||||
import com.googlecode.lanterna.gui2.dialogs.MessageDialogButton
|
||||
import com.googlecode.lanterna.gui2.dialogs.TextInputDialog
|
||||
import com.googlecode.lanterna.gui2.table.Table
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.Persona
|
||||
@@ -48,7 +49,7 @@ class TrustListView extends BasicWindow {
|
||||
Panel topPanel = new Panel()
|
||||
topPanel.setLayoutManager(new GridLayout(2))
|
||||
|
||||
trusted = new Table("Trusted User","Your Trust")
|
||||
trusted = new Table("Trusted User","Reason","Your Trust")
|
||||
trusted.with {
|
||||
setCellSelection(false)
|
||||
setTableModel(model.trustedTableModel)
|
||||
@@ -57,7 +58,7 @@ class TrustListView extends BasicWindow {
|
||||
trusted.setSelectAction({ actionsForUser(true) })
|
||||
topPanel.addComponent(trusted, layoutData)
|
||||
|
||||
distrusted = new Table("Distrusted User", "Your Trust")
|
||||
distrusted = new Table("Distrusted User","Reason", "Your Trust")
|
||||
distrusted.with {
|
||||
setCellSelection(false)
|
||||
setTableModel(model.distrustedTableModel)
|
||||
@@ -90,7 +91,8 @@ class TrustListView extends BasicWindow {
|
||||
LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER)
|
||||
|
||||
Button trustButton = new Button("Trust",{
|
||||
core.eventBus.publish(new TrustEvent(persona : persona, level : TrustLevel.TRUSTED))
|
||||
String reason = TextInputDialog.showDialog(textGUI, "Reason", "Enter reason (optional)", "")
|
||||
core.eventBus.publish(new TrustEvent(persona : persona, level : TrustLevel.TRUSTED, reason : reason))
|
||||
MessageDialog.showMessageDialog(textGUI, "Marked Trusted", persona.getHumanReadableName() + "has been marked trusted",
|
||||
MessageDialogButton.OK)
|
||||
})
|
||||
@@ -100,7 +102,8 @@ class TrustListView extends BasicWindow {
|
||||
MessageDialogButton.OK)
|
||||
})
|
||||
Button distrustButton = new Button("Distrust",{
|
||||
core.eventBus.publish(new TrustEvent(persona : persona, level : TrustLevel.DISTRUSTED))
|
||||
String reason = TextInputDialog.showDialog(textGUI, "Reason", "Enter reason (optional)", "")
|
||||
core.eventBus.publish(new TrustEvent(persona : persona, level : TrustLevel.DISTRUSTED, reason : reason))
|
||||
MessageDialog.showMessageDialog(textGUI, "Marked Distrusted", persona.getHumanReadableName() + "has been marked distrusted",
|
||||
MessageDialogButton.OK)
|
||||
})
|
||||
|
@@ -17,8 +17,8 @@ class TrustModel {
|
||||
this.guiThread = guiThread
|
||||
this.core = core
|
||||
|
||||
modelTrusted = new TableModel("Trusted Users")
|
||||
modelDistrusted = new TableModel("Distrusted Users")
|
||||
modelTrusted = new TableModel("Trusted Users","Reason")
|
||||
modelDistrusted = new TableModel("Distrusted Users","Reason")
|
||||
modelSubscriptions = new TableModel("Name","Trusted","Distrusted","Status","Last Updated")
|
||||
|
||||
core.eventBus.register(TrustEvent.class, this)
|
||||
@@ -57,11 +57,11 @@ class TrustModel {
|
||||
subsRows.times { modelSubscriptions.removeRow(0) }
|
||||
|
||||
core.trustService.good.values().each {
|
||||
modelTrusted.addRow(new PersonaWrapper(it))
|
||||
modelTrusted.addRow(new PersonaWrapper(it.persona),it.reason)
|
||||
}
|
||||
|
||||
core.trustService.bad.values().each {
|
||||
modelDistrusted.addRow(new PersonaWrapper(it))
|
||||
modelDistrusted.addRow(new PersonaWrapper(it.persona),it.reason)
|
||||
}
|
||||
|
||||
core.trustSubscriber.remoteTrustLists.values().each {
|
||||
|
@@ -11,6 +11,7 @@ import com.googlecode.lanterna.gui2.Window
|
||||
import com.googlecode.lanterna.gui2.dialogs.MessageDialog
|
||||
import com.googlecode.lanterna.gui2.dialogs.MessageDialogBuilder
|
||||
import com.googlecode.lanterna.gui2.dialogs.MessageDialogButton
|
||||
import com.googlecode.lanterna.gui2.dialogs.TextInputDialog
|
||||
import com.googlecode.lanterna.gui2.GridLayout.Alignment
|
||||
import com.googlecode.lanterna.gui2.Label
|
||||
import com.googlecode.lanterna.gui2.table.Table
|
||||
@@ -44,14 +45,14 @@ class TrustView extends BasicWindow {
|
||||
Panel topPanel = new Panel()
|
||||
topPanel.setLayoutManager(new GridLayout(2))
|
||||
|
||||
trusted = new Table("Trusted Users")
|
||||
trusted = new Table("Trusted Users","Reason")
|
||||
trusted.setCellSelection(false)
|
||||
trusted.setSelectAction({trustedActions()})
|
||||
trusted.setTableModel(model.modelTrusted)
|
||||
trusted.setVisibleRows(tableSize)
|
||||
topPanel.addComponent(trusted, layoutData)
|
||||
|
||||
distrusted = new Table("Distrusted users")
|
||||
distrusted = new Table("Distrusted users","Reason")
|
||||
distrusted.setCellSelection(false)
|
||||
distrusted.setSelectAction({distrustedActions()})
|
||||
distrusted.setTableModel(model.modelDistrusted)
|
||||
@@ -106,7 +107,8 @@ class TrustView extends BasicWindow {
|
||||
MessageDialogButton.OK)
|
||||
})
|
||||
Button markDistrusted = new Button("Mark Distrusted", {
|
||||
core.eventBus.publish(new TrustEvent(persona : persona, level : TrustLevel.DISTRUSTED))
|
||||
String reason = TextInputDialog.showDialog(textGUI, "Reason", "Enter reason (optional)", "")
|
||||
core.eventBus.publish(new TrustEvent(persona : persona, level : TrustLevel.DISTRUSTED, reason : reason))
|
||||
MessageDialog.showMessageDialog(textGUI, "Marked Distrusted", persona.getHumanReadableName() + "has been marked distrusted",
|
||||
MessageDialogButton.OK)
|
||||
})
|
||||
@@ -122,7 +124,7 @@ class TrustView extends BasicWindow {
|
||||
}
|
||||
|
||||
private void distrustedActions() {
|
||||
int selectedRow = trusted.getSelectedRow()
|
||||
int selectedRow = distrusted.getSelectedRow()
|
||||
def row = model.modelDistrusted.getRow(selectedRow)
|
||||
Persona persona = row[0].persona
|
||||
|
||||
@@ -138,7 +140,8 @@ class TrustView extends BasicWindow {
|
||||
MessageDialogButton.OK)
|
||||
})
|
||||
Button markDistrusted = new Button("Mark Trusted", {
|
||||
core.eventBus.publish(new TrustEvent(persona : persona, level : TrustLevel.TRUSTED))
|
||||
String reason = TextInputDialog.showDialog(textGUI, "Reason", "Enter reason (optional)", "")
|
||||
core.eventBus.publish(new TrustEvent(persona : persona, level : TrustLevel.TRUSTED, reason : reason))
|
||||
MessageDialog.showMessageDialog(textGUI, "Marked Trusted", persona.getHumanReadableName() + "has been marked trusted",
|
||||
MessageDialogButton.OK)
|
||||
})
|
||||
|
@@ -6,7 +6,7 @@ buildscript {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.github.jengelman.gradle.plugins:shadow:2.0.4'
|
||||
classpath 'com.github.jengelman.gradle.plugins:shadow:5.2.0'
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -3,6 +3,12 @@ package com.muwire.core
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
import com.muwire.core.chat.ChatDisconnectionEvent
|
||||
import com.muwire.core.chat.ChatManager
|
||||
import com.muwire.core.chat.ChatMessageEvent
|
||||
import com.muwire.core.chat.ChatServer
|
||||
import com.muwire.core.chat.UIConnectChatEvent
|
||||
import com.muwire.core.chat.UIDisconnectChatEvent
|
||||
import com.muwire.core.connection.ConnectionAcceptor
|
||||
import com.muwire.core.connection.ConnectionEstablisher
|
||||
import com.muwire.core.connection.ConnectionEvent
|
||||
@@ -105,6 +111,8 @@ public class Core {
|
||||
final UploadManager uploadManager
|
||||
final ContentManager contentManager
|
||||
final CertificateManager certificateManager
|
||||
final ChatServer chatServer
|
||||
final ChatManager chatManager
|
||||
|
||||
private final Router router
|
||||
|
||||
@@ -278,8 +286,16 @@ public class Core {
|
||||
CertificateClient certificateClient = new CertificateClient(eventBus, i2pConnector)
|
||||
eventBus.register(UIFetchCertificatesEvent.class, certificateClient)
|
||||
|
||||
log.info("initializing chat server")
|
||||
chatServer = new ChatServer(eventBus, props, trustService, me, spk)
|
||||
eventBus.with {
|
||||
register(ChatMessageEvent.class, chatServer)
|
||||
register(ChatDisconnectionEvent.class, chatServer)
|
||||
register(TrustEvent.class, chatServer)
|
||||
}
|
||||
|
||||
log.info "initializing results sender"
|
||||
ResultsSender resultsSender = new ResultsSender(eventBus, i2pConnector, me, props, certificateManager)
|
||||
ResultsSender resultsSender = new ResultsSender(eventBus, i2pConnector, me, props, certificateManager, chatServer)
|
||||
|
||||
log.info "initializing search manager"
|
||||
SearchManager searchManager = new SearchManager(eventBus, me, resultsSender)
|
||||
@@ -302,11 +318,21 @@ public class Core {
|
||||
log.info("initializing connection establisher")
|
||||
connectionEstablisher = new ConnectionEstablisher(eventBus, i2pConnector, props, connectionManager, hostCache)
|
||||
|
||||
|
||||
log.info("initializing chat manager")
|
||||
chatManager = new ChatManager(eventBus, me, i2pConnector, trustService, props)
|
||||
eventBus.with {
|
||||
register(UIConnectChatEvent.class, chatManager)
|
||||
register(UIDisconnectChatEvent.class, chatManager)
|
||||
register(ChatMessageEvent.class, chatManager)
|
||||
register(ChatDisconnectionEvent.class, chatManager)
|
||||
}
|
||||
|
||||
log.info("initializing acceptor")
|
||||
I2PAcceptor i2pAcceptor = new I2PAcceptor(socketManager)
|
||||
connectionAcceptor = new ConnectionAcceptor(eventBus, connectionManager, props,
|
||||
i2pAcceptor, hostCache, trustService, searchManager, uploadManager, fileManager, connectionEstablisher,
|
||||
certificateManager)
|
||||
certificateManager, chatServer)
|
||||
|
||||
log.info("initializing directory watcher")
|
||||
directoryWatcher = new DirectoryWatcher(eventBus, fileManager, home, props)
|
||||
@@ -358,9 +384,9 @@ public class Core {
|
||||
saveMuSettings()
|
||||
log.info("shutting down trust subscriber")
|
||||
trustSubscriber.stop()
|
||||
log.info("shutting down download manageer")
|
||||
log.info("shutting down download manager")
|
||||
downloadManager.shutdown()
|
||||
log.info("shutting down connection acceeptor")
|
||||
log.info("shutting down connection acceptor")
|
||||
connectionAcceptor.stop()
|
||||
log.info("shutting down connection establisher")
|
||||
connectionEstablisher.stop()
|
||||
@@ -368,6 +394,10 @@ public class Core {
|
||||
directoryWatcher.stop()
|
||||
log.info("shutting down cache client")
|
||||
cacheClient.stop()
|
||||
log.info("shutting down chat server")
|
||||
chatServer.stop()
|
||||
log.info("shutting down chat manager")
|
||||
chatManager.shutdown()
|
||||
log.info("shutting down connection manager")
|
||||
connectionManager.shutdown()
|
||||
if (router != null) {
|
||||
@@ -406,7 +436,7 @@ public class Core {
|
||||
}
|
||||
}
|
||||
|
||||
Core core = new Core(props, home, "0.5.9")
|
||||
Core core = new Core(props, home, "0.6.6")
|
||||
core.startServices()
|
||||
|
||||
// ... at the end, sleep or execute script
|
||||
|
@@ -31,6 +31,9 @@ class MuWireSettings {
|
||||
boolean shareHiddenFiles
|
||||
boolean searchComments
|
||||
boolean browseFiles
|
||||
boolean startChatServer
|
||||
int maxChatConnections
|
||||
boolean advertiseChat
|
||||
Set<String> watchedDirectories
|
||||
float downloadSequentialRatio
|
||||
int hostClearInterval, hostHopelessInterval, hostRejectInterval
|
||||
@@ -79,7 +82,10 @@ class MuWireSettings {
|
||||
speedSmoothSeconds = Integer.valueOf(props.getProperty("speedSmoothSeconds","60"))
|
||||
totalUploadSlots = Integer.valueOf(props.getProperty("totalUploadSlots","-1"))
|
||||
uploadSlotsPerUser = Integer.valueOf(props.getProperty("uploadSlotsPerUser","-1"))
|
||||
|
||||
startChatServer = Boolean.valueOf(props.getProperty("startChatServer","false"))
|
||||
maxChatConnections = Integer.valueOf(props.get("maxChatConnections", "-1"))
|
||||
advertiseChat = Boolean.valueOf(props.getProperty("advertiseChat","true"))
|
||||
|
||||
watchedDirectories = DataUtil.readEncodedSet(props, "watchedDirectories")
|
||||
watchedKeywords = DataUtil.readEncodedSet(props, "watchedKeywords")
|
||||
watchedRegexes = DataUtil.readEncodedSet(props, "watchedRegexes")
|
||||
@@ -127,6 +133,9 @@ class MuWireSettings {
|
||||
props.setProperty("speedSmoothSeconds", String.valueOf(speedSmoothSeconds))
|
||||
props.setProperty("totalUploadSlots", String.valueOf(totalUploadSlots))
|
||||
props.setProperty("uploadSlotsPerUser", String.valueOf(uploadSlotsPerUser))
|
||||
props.setProperty("startChatServer", String.valueOf(startChatServer))
|
||||
props.setProperty("maxChatConnectios", String.valueOf(maxChatConnections))
|
||||
props.setProperty("advertiseChat", String.valueOf(advertiseChat))
|
||||
|
||||
DataUtil.writeEncodedSet(watchedDirectories, "watchedDirectories", props)
|
||||
DataUtil.writeEncodedSet(watchedKeywords, "watchedKeywords", props)
|
||||
|
24
core/src/main/groovy/com/muwire/core/chat/ChatAction.java
Normal file
24
core/src/main/groovy/com/muwire/core/chat/ChatAction.java
Normal file
@@ -0,0 +1,24 @@
|
||||
package com.muwire.core.chat;
|
||||
|
||||
enum ChatAction {
|
||||
JOIN(true, false, true, false),
|
||||
LEAVE(false, false, true, false),
|
||||
SAY(false, false, true, false),
|
||||
LIST(true, true, true, false),
|
||||
HELP(true, true, true, false),
|
||||
INFO(true, true, true, false),
|
||||
JOINED(true, true, false, false),
|
||||
TRUST(true, false, true, true),
|
||||
DISTRUST(true, false, true, true);
|
||||
|
||||
final boolean console;
|
||||
final boolean stateless;
|
||||
final boolean user;
|
||||
final boolean local;
|
||||
ChatAction(boolean console, boolean stateless, boolean user, boolean local) {
|
||||
this.console = console;
|
||||
this.stateless = stateless;
|
||||
this.user = user;
|
||||
this.local = local;
|
||||
}
|
||||
}
|
142
core/src/main/groovy/com/muwire/core/chat/ChatClient.groovy
Normal file
142
core/src/main/groovy/com/muwire/core/chat/ChatClient.groovy
Normal file
@@ -0,0 +1,142 @@
|
||||
package com.muwire.core.chat
|
||||
|
||||
import java.lang.System.Logger.Level
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util.concurrent.Executor
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
import com.muwire.core.Constants
|
||||
import com.muwire.core.EventBus
|
||||
import com.muwire.core.MuWireSettings
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.connection.Endpoint
|
||||
import com.muwire.core.connection.I2PConnector
|
||||
import com.muwire.core.trust.TrustService
|
||||
import com.muwire.core.util.DataUtil
|
||||
|
||||
import groovy.util.logging.Log
|
||||
|
||||
@Log
|
||||
class ChatClient implements Closeable {
|
||||
|
||||
private static final long REJECTION_BACKOFF = 60 * 1000
|
||||
|
||||
private static final Executor CONNECTOR = Executors.newCachedThreadPool()
|
||||
|
||||
private final I2PConnector connector
|
||||
private final EventBus eventBus
|
||||
private final Persona host, me
|
||||
private final TrustService trustService
|
||||
private final MuWireSettings settings
|
||||
|
||||
private ChatConnection connection
|
||||
private boolean connectInProgress
|
||||
private long lastRejectionTime
|
||||
private Thread connectThread
|
||||
|
||||
ChatClient(I2PConnector connector, EventBus eventBus, Persona host, Persona me, TrustService trustService,
|
||||
MuWireSettings settings) {
|
||||
this.connector = connector
|
||||
this.eventBus = eventBus
|
||||
this.host = host
|
||||
this.me = me
|
||||
this.trustService = trustService
|
||||
this.settings = settings
|
||||
}
|
||||
|
||||
synchronized void connectIfNeeded() {
|
||||
if (connection != null || connectInProgress || (System.currentTimeMillis() - lastRejectionTime < REJECTION_BACKOFF))
|
||||
return
|
||||
connectInProgress = true
|
||||
CONNECTOR.execute({connect()})
|
||||
}
|
||||
|
||||
private void connect() {
|
||||
synchronized(this) {
|
||||
if (!connectInProgress)
|
||||
return
|
||||
connectThread = Thread.currentThread()
|
||||
}
|
||||
Endpoint endpoint = null
|
||||
try {
|
||||
eventBus.publish(new ChatConnectionEvent(status : ChatConnectionAttemptStatus.CONNECTING, persona : host))
|
||||
endpoint = connector.connect(host.destination)
|
||||
DataOutputStream dos = new DataOutputStream(endpoint.getOutputStream())
|
||||
DataInputStream dis = new DataInputStream(endpoint.getInputStream())
|
||||
|
||||
dos.with {
|
||||
write("IRC\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
write("Version:${Constants.CHAT_VERSION}\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
write("Persona:${me.toBase64()}\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
write("\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
flush()
|
||||
}
|
||||
|
||||
String codeString = DataUtil.readTillRN(dis)
|
||||
int code = Integer.parseInt(codeString.split(" ")[0])
|
||||
|
||||
if (code == 429) {
|
||||
eventBus.publish(new ChatConnectionEvent(status : ChatConnectionAttemptStatus.REJECTED, persona : host))
|
||||
try { dos.close() } catch (IOException ignore) {}
|
||||
endpoint.close()
|
||||
synchronized(this) {
|
||||
lastRejectionTime = System.currentTimeMillis()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (code != 200)
|
||||
throw new Exception("unknown code $code")
|
||||
|
||||
Map<String,String> headers = DataUtil.readAllHeaders(dis)
|
||||
if (!headers.containsKey('Version'))
|
||||
throw new Exception("Version header missing")
|
||||
|
||||
int version = Integer.parseInt(headers['Version'])
|
||||
if (version != Constants.CHAT_VERSION)
|
||||
throw new Exception("Unknown chat version $version")
|
||||
|
||||
synchronized(this) {
|
||||
if (!connectInProgress)
|
||||
return
|
||||
connection = new ChatConnection(eventBus, endpoint, host, false, trustService, settings)
|
||||
connection.start()
|
||||
}
|
||||
eventBus.publish(new ChatConnectionEvent(status : ChatConnectionAttemptStatus.SUCCESSFUL, persona : host,
|
||||
connection : connection))
|
||||
} catch (Exception e) {
|
||||
log.log(java.util.logging.Level.WARNING, "connect failed", e)
|
||||
eventBus.publish(new ChatConnectionEvent(status : ChatConnectionAttemptStatus.FAILED, persona : host))
|
||||
if (endpoint != null) {
|
||||
try {endpoint.getOutputStream().close() } catch (IOException ignore) {}
|
||||
endpoint.close()
|
||||
}
|
||||
} finally {
|
||||
synchronized(this) {
|
||||
connectInProgress = false
|
||||
connectThread = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
synchronized void disconnected() {
|
||||
connectInProgress = false
|
||||
connection = null
|
||||
}
|
||||
|
||||
@Override
|
||||
synchronized public void close() {
|
||||
connectInProgress = false
|
||||
connectThread?.interrupt()
|
||||
connection?.close()
|
||||
eventBus.publish(new ChatConnectionEvent(status : ChatConnectionAttemptStatus.DISCONNECTED, persona : host))
|
||||
}
|
||||
|
||||
synchronized void ping() {
|
||||
connection?.sendPing()
|
||||
}
|
||||
|
||||
synchronized void sendChat(ChatMessageEvent e) {
|
||||
connection?.sendChat(e)
|
||||
}
|
||||
}
|
28
core/src/main/groovy/com/muwire/core/chat/ChatCommand.groovy
Normal file
28
core/src/main/groovy/com/muwire/core/chat/ChatCommand.groovy
Normal file
@@ -0,0 +1,28 @@
|
||||
package com.muwire.core.chat
|
||||
|
||||
class ChatCommand {
|
||||
private final ChatAction action
|
||||
private final String payload
|
||||
final String source
|
||||
ChatCommand(String source) {
|
||||
if (source.charAt(0) != '/')
|
||||
throw new Exception("command doesn't start with / $source")
|
||||
|
||||
int position = 1
|
||||
StringBuilder sb = new StringBuilder()
|
||||
while(position < source.length()) {
|
||||
char c = source.charAt(position)
|
||||
if (c == ' ')
|
||||
break
|
||||
sb.append(c)
|
||||
position++
|
||||
}
|
||||
String command = sb.toString().toUpperCase()
|
||||
action = ChatAction.valueOf(command)
|
||||
if (position < source.length())
|
||||
payload = source.substring(position + 1)
|
||||
else
|
||||
payload = ""
|
||||
this.source = source
|
||||
}
|
||||
}
|
280
core/src/main/groovy/com/muwire/core/chat/ChatConnection.groovy
Normal file
280
core/src/main/groovy/com/muwire/core/chat/ChatConnection.groovy
Normal file
@@ -0,0 +1,280 @@
|
||||
package com.muwire.core.chat
|
||||
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util.concurrent.BlockingQueue
|
||||
import java.util.concurrent.LinkedBlockingQueue
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.logging.Level
|
||||
|
||||
import com.muwire.core.Constants
|
||||
import com.muwire.core.EventBus
|
||||
import com.muwire.core.MuWireSettings
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.connection.Endpoint
|
||||
import com.muwire.core.trust.TrustLevel
|
||||
import com.muwire.core.trust.TrustService
|
||||
|
||||
import groovy.json.JsonOutput
|
||||
import groovy.json.JsonSlurper
|
||||
import groovy.util.logging.Log
|
||||
import net.i2p.crypto.DSAEngine
|
||||
import net.i2p.data.Base64
|
||||
import net.i2p.data.Signature
|
||||
import net.i2p.data.SigningPrivateKey
|
||||
|
||||
@Log
|
||||
class ChatConnection implements ChatLink {
|
||||
|
||||
private static final long PING_INTERVAL = 20000
|
||||
private static final long MAX_CHAT_AGE = 5 * 60 * 1000
|
||||
|
||||
private final EventBus eventBus
|
||||
private final Endpoint endpoint
|
||||
private final Persona persona
|
||||
private final boolean incoming
|
||||
private final TrustService trustService
|
||||
private final MuWireSettings settings
|
||||
|
||||
private final AtomicBoolean running = new AtomicBoolean()
|
||||
private final BlockingQueue messages = new LinkedBlockingQueue()
|
||||
private final Thread reader, writer
|
||||
private final LinkedList<Long> timestamps = new LinkedList<>()
|
||||
private final BlockingQueue incomingEvents = new LinkedBlockingQueue()
|
||||
|
||||
private final DataInputStream dis
|
||||
private final DataOutputStream dos
|
||||
|
||||
private final JsonSlurper slurper = new JsonSlurper()
|
||||
|
||||
private volatile long lastPingSentTime
|
||||
|
||||
ChatConnection(EventBus eventBus, Endpoint endpoint, Persona persona, boolean incoming,
|
||||
TrustService trustService, MuWireSettings settings) {
|
||||
this.eventBus = eventBus
|
||||
this.endpoint = endpoint
|
||||
this.persona = persona
|
||||
this.incoming = incoming
|
||||
this.trustService = trustService
|
||||
this.settings = settings
|
||||
|
||||
this.dis = new DataInputStream(endpoint.getInputStream())
|
||||
this.dos = new DataOutputStream(endpoint.getOutputStream())
|
||||
|
||||
this.reader = new Thread({readLoop()} as Runnable)
|
||||
this.reader.setName("reader-${persona.getHumanReadableName()}")
|
||||
this.reader.setDaemon(true)
|
||||
|
||||
this.writer = new Thread({writeLoop()} as Runnable)
|
||||
this.writer.setName("writer-${persona.getHumanReadableName()}")
|
||||
this.writer.setDaemon(true)
|
||||
}
|
||||
|
||||
void start() {
|
||||
if (!running.compareAndSet(false, true)) {
|
||||
log.log(Level.WARNING,"${persona.getHumanReadableName()} already running", new Exception())
|
||||
return
|
||||
}
|
||||
reader.start()
|
||||
writer.start()
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isUp() {
|
||||
running.get()
|
||||
}
|
||||
|
||||
@Override
|
||||
public Persona getPersona() {
|
||||
persona
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (!running.compareAndSet(true, false)) {
|
||||
log.log(Level.WARNING,"${persona.getHumanReadableName()} already closed", new Exception())
|
||||
return
|
||||
}
|
||||
log.info("Closing "+persona.getHumanReadableName())
|
||||
reader.interrupt()
|
||||
writer.interrupt()
|
||||
endpoint.close()
|
||||
eventBus.publish(new ChatDisconnectionEvent(persona : persona))
|
||||
}
|
||||
|
||||
private void readLoop() {
|
||||
try {
|
||||
while(running.get())
|
||||
read()
|
||||
} catch( InterruptedException | SocketTimeoutException ignored) {
|
||||
} catch (Exception e) {
|
||||
log.log(Level.WARNING,"unhandled exception in reader", e)
|
||||
} finally {
|
||||
try {endpoint.getOutputStream().close()} catch (IOException ignore) {}
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
||||
private void writeLoop() {
|
||||
try {
|
||||
while(running.get()) {
|
||||
def message = messages.take()
|
||||
write(message)
|
||||
}
|
||||
} catch (InterruptedException ignore) {
|
||||
} catch (Exception e) {
|
||||
log.log(Level.WARNING,"unhandled exception in writer",e)
|
||||
} finally {
|
||||
try {endpoint.getOutputStream().close()} catch (IOException ignore) {}
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
||||
private void read() {
|
||||
int length = dis.readUnsignedShort()
|
||||
byte [] payload = new byte[length]
|
||||
dis.readFully(payload)
|
||||
def json = slurper.parse(payload)
|
||||
if (json.type == null)
|
||||
throw new Exception("missing json type")
|
||||
switch(json.type) {
|
||||
case "Ping" : break // just ignore
|
||||
case "Chat" : handleChat(json); break
|
||||
case "Leave": handleLeave(json); break
|
||||
default :
|
||||
throw new Exception("unknown json type ${json.type}")
|
||||
}
|
||||
}
|
||||
|
||||
private void write(Object message) {
|
||||
byte [] payload = JsonOutput.toJson(message).bytes
|
||||
dos.with {
|
||||
writeShort(payload.length)
|
||||
write(payload)
|
||||
flush()
|
||||
}
|
||||
}
|
||||
|
||||
void sendPing() {
|
||||
long now = System.currentTimeMillis()
|
||||
if (now - lastPingSentTime < PING_INTERVAL)
|
||||
return
|
||||
def ping = [:]
|
||||
ping.type = "Ping"
|
||||
ping.version = 1
|
||||
messages.put(ping)
|
||||
lastPingSentTime = now
|
||||
}
|
||||
|
||||
private void handleChat(def json) {
|
||||
UUID uuid = UUID.fromString(json.uuid)
|
||||
Persona host = fromString(json.host)
|
||||
Persona sender = fromString(json.sender)
|
||||
long chatTime = json.chatTime
|
||||
String room = json.room
|
||||
String payload = json.payload
|
||||
byte [] sig = Base64.decode(json.sig)
|
||||
|
||||
if (!verify(uuid,host,sender,chatTime,room,payload,sig)) {
|
||||
log.warning("chat didn't verify")
|
||||
return
|
||||
}
|
||||
if (incoming) {
|
||||
if (sender.destination != endpoint.destination) {
|
||||
log.warning("Sender destination mismatch, dropping message")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if (host.destination != endpoint.destination) {
|
||||
log.warning("Host destination mismatch, dropping message")
|
||||
return
|
||||
}
|
||||
}
|
||||
if (System.currentTimeMillis() - chatTime > MAX_CHAT_AGE) {
|
||||
log.warning("Chat too old, dropping")
|
||||
return
|
||||
}
|
||||
switch(trustService.getLevel(sender.destination)) {
|
||||
case TrustLevel.TRUSTED : break
|
||||
case TrustLevel.NEUTRAL :
|
||||
if (!settings.allowUntrusted)
|
||||
return
|
||||
else
|
||||
break
|
||||
case TrustLevel.DISTRUSTED :
|
||||
return
|
||||
}
|
||||
def event = new ChatMessageEvent( uuid : uuid, payload : payload, sender : sender,
|
||||
host : host, room : room, chatTime : chatTime, sig : sig)
|
||||
eventBus.publish(event)
|
||||
if (!incoming)
|
||||
incomingEvents.put(event)
|
||||
}
|
||||
|
||||
private void handleLeave(def json) {
|
||||
Persona leaver = fromString(json.persona)
|
||||
eventBus.publish(new UserDisconnectedEvent(user : leaver, host : persona))
|
||||
incomingEvents.put(leaver)
|
||||
}
|
||||
|
||||
private static Persona fromString(String base64) {
|
||||
new Persona(new ByteArrayInputStream(Base64.decode(base64)))
|
||||
}
|
||||
|
||||
private static boolean verify(UUID uuid, Persona host, Persona sender, long chatTime,
|
||||
String room, String payload, byte []sig) {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream()
|
||||
DataOutputStream daos = new DataOutputStream(baos)
|
||||
daos.write(uuid.toString().bytes)
|
||||
host.write(daos)
|
||||
sender.write(daos)
|
||||
daos.writeLong(chatTime)
|
||||
daos.write(room.getBytes(StandardCharsets.UTF_8))
|
||||
daos.write(payload.getBytes(StandardCharsets.UTF_8))
|
||||
daos.close()
|
||||
byte [] signed = baos.toByteArray()
|
||||
def spk = sender.destination.getSigningPublicKey()
|
||||
def signature = new Signature(Constants.SIG_TYPE, sig)
|
||||
DSAEngine.getInstance().verifySignature(signature, signed, spk)
|
||||
}
|
||||
|
||||
public static byte[] sign(UUID uuid, long chatTime, String room, String words, Persona sender, Persona host, SigningPrivateKey spk) {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream()
|
||||
DataOutputStream daos = new DataOutputStream(baos)
|
||||
daos.with {
|
||||
write(uuid.toString().bytes)
|
||||
host.write(daos)
|
||||
sender.write(daos)
|
||||
writeLong(chatTime)
|
||||
write(room.getBytes(StandardCharsets.UTF_8))
|
||||
write(words.getBytes(StandardCharsets.UTF_8))
|
||||
close()
|
||||
}
|
||||
byte [] payload = baos.toByteArray()
|
||||
Signature sig = DSAEngine.getInstance().sign(payload, spk)
|
||||
sig.getData()
|
||||
}
|
||||
|
||||
void sendChat(ChatMessageEvent e) {
|
||||
def chat = [:]
|
||||
chat.type = "Chat"
|
||||
chat.uuid = e.uuid.toString()
|
||||
chat.host = e.host.toBase64()
|
||||
chat.sender = e.sender.toBase64()
|
||||
chat.chatTime = e.chatTime
|
||||
chat.room = e.room
|
||||
chat.payload = e.payload
|
||||
chat.sig = Base64.encode(e.sig)
|
||||
messages.put(chat)
|
||||
}
|
||||
|
||||
void sendLeave(Persona p) {
|
||||
def leave = [:]
|
||||
leave.type = "Leave"
|
||||
leave.persona = p.toBase64()
|
||||
messages.put(leave)
|
||||
}
|
||||
|
||||
public Object nextEvent() {
|
||||
incomingEvents.take()
|
||||
}
|
||||
}
|
@@ -0,0 +1,5 @@
|
||||
package com.muwire.core.chat;
|
||||
|
||||
public enum ChatConnectionAttemptStatus {
|
||||
CONNECTING, SUCCESSFUL, REJECTED, FAILED, DISCONNECTED
|
||||
}
|
@@ -0,0 +1,14 @@
|
||||
package com.muwire.core.chat
|
||||
|
||||
import com.muwire.core.Event
|
||||
import com.muwire.core.Persona
|
||||
|
||||
class ChatConnectionEvent extends Event {
|
||||
ChatConnectionAttemptStatus status
|
||||
Persona persona
|
||||
ChatLink connection
|
||||
|
||||
public String toString() {
|
||||
super.toString() + " " + persona.getHumanReadableName() + " " + status.toString()
|
||||
}
|
||||
}
|
@@ -0,0 +1,8 @@
|
||||
package com.muwire.core.chat
|
||||
|
||||
import com.muwire.core.Event
|
||||
import com.muwire.core.Persona
|
||||
|
||||
class ChatDisconnectionEvent extends Event {
|
||||
Persona persona
|
||||
}
|
14
core/src/main/groovy/com/muwire/core/chat/ChatLink.java
Normal file
14
core/src/main/groovy/com/muwire/core/chat/ChatLink.java
Normal file
@@ -0,0 +1,14 @@
|
||||
package com.muwire.core.chat;
|
||||
|
||||
import java.io.Closeable;
|
||||
|
||||
import com.muwire.core.Persona;
|
||||
|
||||
public interface ChatLink extends Closeable {
|
||||
public Persona getPersona();
|
||||
public boolean isUp();
|
||||
public void sendChat(ChatMessageEvent e);
|
||||
public void sendLeave(Persona p);
|
||||
public void sendPing();
|
||||
public Object nextEvent() throws InterruptedException;
|
||||
}
|
73
core/src/main/groovy/com/muwire/core/chat/ChatManager.groovy
Normal file
73
core/src/main/groovy/com/muwire/core/chat/ChatManager.groovy
Normal file
@@ -0,0 +1,73 @@
|
||||
package com.muwire.core.chat
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
import com.muwire.core.EventBus
|
||||
import com.muwire.core.MuWireSettings
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.connection.I2PConnector
|
||||
import com.muwire.core.trust.TrustService
|
||||
|
||||
class ChatManager {
|
||||
private final EventBus eventBus
|
||||
private final Persona me
|
||||
private final I2PConnector connector
|
||||
private final TrustService trustService
|
||||
private final MuWireSettings settings
|
||||
|
||||
private final Map<Persona, ChatClient> clients = new ConcurrentHashMap<>()
|
||||
|
||||
ChatManager(EventBus eventBus, Persona me, I2PConnector connector, TrustService trustService,
|
||||
MuWireSettings settings) {
|
||||
this.eventBus = eventBus
|
||||
this.me = me
|
||||
this.connector = connector
|
||||
this.trustService = trustService
|
||||
this.settings = settings
|
||||
|
||||
Timer timer = new Timer("chat-connector", true)
|
||||
timer.schedule({connect()} as TimerTask, 1000, 1000)
|
||||
}
|
||||
|
||||
void onUIConnectChatEvent(UIConnectChatEvent e) {
|
||||
if (e.host == me) {
|
||||
eventBus.publish(new ChatConnectionEvent(status : ChatConnectionAttemptStatus.SUCCESSFUL,
|
||||
persona : me, connection : LocalChatLink.INSTANCE))
|
||||
} else {
|
||||
ChatClient client = new ChatClient(connector, eventBus, e.host, me, trustService, settings)
|
||||
clients.put(e.host, client)
|
||||
}
|
||||
}
|
||||
|
||||
void onUIDisconnectChatEvent(UIDisconnectChatEvent e) {
|
||||
if (e.host == me)
|
||||
return
|
||||
ChatClient client = clients.remove(e.host)
|
||||
client?.close()
|
||||
}
|
||||
|
||||
void onChatMessageEvent(ChatMessageEvent e) {
|
||||
if (e.host == me)
|
||||
return
|
||||
if (e.sender != me)
|
||||
return
|
||||
clients[e.host]?.sendChat(e)
|
||||
}
|
||||
|
||||
void onChatDisconnectionEvent(ChatDisconnectionEvent e) {
|
||||
clients[e.persona]?.disconnected()
|
||||
}
|
||||
|
||||
private void connect() {
|
||||
clients.each { k, v ->
|
||||
v.connectIfNeeded()
|
||||
v.ping()
|
||||
}
|
||||
}
|
||||
|
||||
void shutdown() {
|
||||
clients.each { k, v ->
|
||||
v.close()
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,13 @@
|
||||
package com.muwire.core.chat
|
||||
|
||||
import com.muwire.core.Event
|
||||
import com.muwire.core.Persona
|
||||
|
||||
class ChatMessageEvent extends Event {
|
||||
UUID uuid
|
||||
String payload
|
||||
Persona sender, host
|
||||
String room
|
||||
long chatTime
|
||||
byte [] sig
|
||||
}
|
321
core/src/main/groovy/com/muwire/core/chat/ChatServer.groovy
Normal file
321
core/src/main/groovy/com/muwire/core/chat/ChatServer.groovy
Normal file
@@ -0,0 +1,321 @@
|
||||
package com.muwire.core.chat
|
||||
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.logging.Level
|
||||
import java.util.stream.Collectors
|
||||
|
||||
import com.muwire.core.Constants
|
||||
import com.muwire.core.EventBus
|
||||
import com.muwire.core.MuWireSettings
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.connection.Endpoint
|
||||
import com.muwire.core.trust.TrustEvent
|
||||
import com.muwire.core.trust.TrustLevel
|
||||
import com.muwire.core.trust.TrustService
|
||||
import com.muwire.core.util.DataUtil
|
||||
|
||||
import groovy.util.logging.Log
|
||||
import net.i2p.data.Base64
|
||||
import net.i2p.data.Destination
|
||||
import net.i2p.data.SigningPrivateKey
|
||||
import net.i2p.util.ConcurrentHashSet
|
||||
|
||||
@Log
|
||||
class ChatServer {
|
||||
public static final String CONSOLE = "__CONSOLE__"
|
||||
private final EventBus eventBus
|
||||
private final MuWireSettings settings
|
||||
private final TrustService trustService
|
||||
private final Persona me
|
||||
private final SigningPrivateKey spk
|
||||
|
||||
private final Map<Destination, ChatLink> connections = new ConcurrentHashMap()
|
||||
private final Map<String, Set<Persona>> rooms = new ConcurrentHashMap<>()
|
||||
private final Map<Persona, Set<String>> memberships = new ConcurrentHashMap<>()
|
||||
private final Map<String, Persona> shortNames = new ConcurrentHashMap<>()
|
||||
|
||||
private final AtomicBoolean running = new AtomicBoolean()
|
||||
|
||||
ChatServer(EventBus eventBus, MuWireSettings settings, TrustService trustService, Persona me, SigningPrivateKey spk) {
|
||||
this.eventBus = eventBus
|
||||
this.settings = settings
|
||||
this.trustService = trustService
|
||||
this.me = me
|
||||
this.spk = spk
|
||||
|
||||
Timer timer = new Timer("chat-server-pinger", true)
|
||||
timer.schedule({sendPings()} as TimerTask, 1000, 1000)
|
||||
}
|
||||
|
||||
public void start() {
|
||||
if (!running.compareAndSet(false, true))
|
||||
return
|
||||
connections.put(me.destination, LocalChatLink.INSTANCE)
|
||||
joinRoom(me, CONSOLE)
|
||||
shortNames.put(me.getHumanReadableName(), me)
|
||||
echo("/SAY Welcome to my chat server! Type /HELP for list of available commands.",me.destination)
|
||||
}
|
||||
|
||||
private void sendPings() {
|
||||
connections.each { k,v ->
|
||||
v.sendPing()
|
||||
}
|
||||
}
|
||||
|
||||
public void handle(Endpoint endpoint) {
|
||||
InputStream is = endpoint.getInputStream()
|
||||
OutputStream os = endpoint.getOutputStream()
|
||||
|
||||
Map<String, String> headers = DataUtil.readAllHeaders(is)
|
||||
|
||||
if (!headers.containsKey("Version"))
|
||||
throw new Exception("Version header missing")
|
||||
|
||||
int version = Integer.parseInt(headers['Version'])
|
||||
if (version != Constants.CHAT_VERSION)
|
||||
throw new Exception("Unknown chat version $version")
|
||||
|
||||
if (!headers.containsKey('Persona'))
|
||||
throw new Exception("Persona header missing")
|
||||
|
||||
Persona client = new Persona(new ByteArrayInputStream(Base64.decode(headers['Persona'])))
|
||||
if (client.destination != endpoint.destination)
|
||||
throw new Exception("Client destination mismatch")
|
||||
|
||||
if (!running.get()) {
|
||||
os.write("400 Chat Not Enabled\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
os.close()
|
||||
endpoint.close()
|
||||
return
|
||||
}
|
||||
|
||||
if (connections.containsKey(client.destination) || connections.size() == settings.maxChatConnections) {
|
||||
os.write("429 Rejected\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
os.close()
|
||||
endpoint.close()
|
||||
return
|
||||
}
|
||||
|
||||
os.with {
|
||||
write("200 OK\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
write("Version:${Constants.CHAT_VERSION}\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
write("\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
flush()
|
||||
}
|
||||
|
||||
ChatConnection connection = new ChatConnection(eventBus, endpoint, client, true, trustService, settings)
|
||||
connections.put(endpoint.destination, connection)
|
||||
joinRoom(client, CONSOLE)
|
||||
shortNames.put(client.getHumanReadableName(), client)
|
||||
connection.start()
|
||||
echo("/SAY Welcome to my chat server! Type /HELP for help on available commands",connection.endpoint.destination)
|
||||
}
|
||||
|
||||
void onChatDisconnectionEvent(ChatDisconnectionEvent e) {
|
||||
ChatConnection con = connections.remove(e.persona.destination)
|
||||
if (con == null)
|
||||
return
|
||||
|
||||
Set<String> rooms = memberships.get(e.persona)
|
||||
if (rooms != null) {
|
||||
rooms.each {
|
||||
leaveRoom(e.persona, it)
|
||||
}
|
||||
}
|
||||
shortNames.remove(e.persona.getHumanReadableName())
|
||||
connections.each { k, v ->
|
||||
v.sendLeave(e.persona)
|
||||
}
|
||||
}
|
||||
|
||||
void onTrustEvent(TrustEvent e) {
|
||||
if (e.level == TrustLevel.TRUSTED)
|
||||
return
|
||||
if (settings.allowUntrusted && e.level == TrustLevel.NEUTRAL)
|
||||
return
|
||||
|
||||
ChatConnection connection = connections.remove(e.persona.destination)
|
||||
connection?.close()
|
||||
}
|
||||
|
||||
private void joinRoom(Persona p, String room) {
|
||||
Set<Persona> existing = rooms.get(room)
|
||||
if (existing == null) {
|
||||
existing = new ConcurrentHashSet<>()
|
||||
rooms.put(room, existing)
|
||||
}
|
||||
existing.add(p)
|
||||
|
||||
Set<String> membership = memberships.get(p)
|
||||
if (membership == null) {
|
||||
membership = new ConcurrentHashSet<>()
|
||||
memberships.put(p, membership)
|
||||
}
|
||||
membership.add(room)
|
||||
}
|
||||
|
||||
private void leaveRoom(Persona p, String room) {
|
||||
Set<Persona> existing = rooms.get(room)
|
||||
if (existing == null) {
|
||||
log.warning(p.getHumanReadableName() + " leaving room they hadn't joined")
|
||||
return
|
||||
}
|
||||
existing.remove(p)
|
||||
if (existing.isEmpty())
|
||||
rooms.remove(room)
|
||||
|
||||
Set<String> membership = memberships.get(p)
|
||||
if (membership == null) {
|
||||
log.warning(p.getHumanReadableName() + " didn't have any memberships")
|
||||
return
|
||||
}
|
||||
membership.remove(room)
|
||||
if (membership.isEmpty())
|
||||
memberships.remove(p)
|
||||
}
|
||||
|
||||
void onChatMessageEvent(ChatMessageEvent e) {
|
||||
if (e.host != me)
|
||||
return
|
||||
|
||||
ChatCommand command
|
||||
try {
|
||||
command = new ChatCommand(e.payload)
|
||||
} catch (Exception badCommand) {
|
||||
log.log(Level.WARNING, "bad chat command",badCommand)
|
||||
return
|
||||
}
|
||||
|
||||
if ((command.action.console && e.room != CONSOLE) ||
|
||||
(!command.action.console && e.room == CONSOLE) ||
|
||||
!command.action.user)
|
||||
return
|
||||
|
||||
if (command.action.local && e.sender != me)
|
||||
return
|
||||
|
||||
switch(command.action) {
|
||||
case ChatAction.JOIN : processJoin(command.payload, e); break
|
||||
case ChatAction.LEAVE : processLeave(e); break
|
||||
case ChatAction.SAY : processSay(e); break
|
||||
case ChatAction.LIST : processList(e.sender.destination); break
|
||||
case ChatAction.INFO : processInfo(e.sender.destination); break
|
||||
case ChatAction.HELP : processHelp(e.sender.destination); break
|
||||
case ChatAction.TRUST : processTrust(command.payload, TrustLevel.TRUSTED); break
|
||||
case ChatAction.DISTRUST : processTrust(command.payload, TrustLevel.DISTRUSTED); break
|
||||
}
|
||||
}
|
||||
|
||||
private void processJoin(String room, ChatMessageEvent e) {
|
||||
joinRoom(e.sender, room)
|
||||
rooms[room].each {
|
||||
if (it == e.sender)
|
||||
return
|
||||
connections[it.destination].sendChat(e)
|
||||
}
|
||||
String payload = rooms[room].stream().filter({it != e.sender}).map({it.toBase64()})
|
||||
.collect(Collectors.joining(","))
|
||||
if (payload.length() == 0) {
|
||||
return
|
||||
}
|
||||
payload = "/JOINED $payload"
|
||||
long now = System.currentTimeMillis()
|
||||
UUID uuid = UUID.randomUUID()
|
||||
byte [] sig = ChatConnection.sign(uuid, now, room, payload, me, me, spk)
|
||||
ChatMessageEvent echo = new ChatMessageEvent(
|
||||
uuid : uuid,
|
||||
payload : payload,
|
||||
sender : me,
|
||||
host : me,
|
||||
room : room,
|
||||
chatTime : now,
|
||||
sig : sig
|
||||
)
|
||||
connections[e.sender.destination].sendChat(echo)
|
||||
}
|
||||
|
||||
private void processLeave(ChatMessageEvent e) {
|
||||
leaveRoom(e.sender, e.room)
|
||||
rooms.getOrDefault(e.room, []).each {
|
||||
if (it == e.sender)
|
||||
return
|
||||
connections[it.destination].sendChat(e)
|
||||
}
|
||||
}
|
||||
|
||||
private void processSay(ChatMessageEvent e) {
|
||||
if (rooms.containsKey(e.room)) {
|
||||
// not a private message
|
||||
rooms[e.room].each {
|
||||
if (it == e.sender)
|
||||
return
|
||||
connections[it.destination].sendChat(e)
|
||||
}
|
||||
} else {
|
||||
Persona target = new Persona(new ByteArrayInputStream(Base64.decode(e.room)))
|
||||
connections[target.destination]?.sendChat(e)
|
||||
}
|
||||
}
|
||||
|
||||
private void processList(Destination d) {
|
||||
String roomList = rooms.keySet().stream().filter({it != CONSOLE}).collect(Collectors.joining("\n"))
|
||||
roomList = "/SAY \nRoom List:\n"+roomList
|
||||
echo(roomList, d)
|
||||
}
|
||||
|
||||
private void processInfo(Destination d) {
|
||||
String info = "/SAY \nThe address of this server is\n========\n${me.toBase64()}\n========\nCopy/paste the above and share it\n"
|
||||
String connectedUsers = memberships.keySet().stream().map({it.getHumanReadableName()}).collect(Collectors.joining("\n"))
|
||||
info = "${info}\nConnected Users:\n$connectedUsers\n======="
|
||||
echo(info, d)
|
||||
}
|
||||
|
||||
private void processHelp(Destination d) {
|
||||
String help = """/SAY
|
||||
Available commands: /JOIN /LEAVE /SAY /LIST /INFO /TRUST /DISTRUST /HELP
|
||||
/JOIN <room name> - joins a room, or creates one if it does not exist. You must type this in the console
|
||||
/LEAVE - leaves a room. You must type this in the room you want to leave
|
||||
/SAY - optional, says something in the room you're in
|
||||
/LIST - lists the existing rooms on this server. You must type this in the console
|
||||
/INFO - shows information about this server. You must type this in the console
|
||||
/TRUST <user> - marks user as trusted. This is only available to the server owner
|
||||
/DISTRUST <user> - marks user as distrusted. This is only available to the server owner
|
||||
/HELP - prints this help message
|
||||
"""
|
||||
echo(help, d)
|
||||
}
|
||||
|
||||
private void echo(String payload, Destination d) {
|
||||
log.info "echoing $payload"
|
||||
UUID uuid = UUID.randomUUID()
|
||||
long now = System.currentTimeMillis()
|
||||
byte [] sig = ChatConnection.sign(uuid, now, CONSOLE, payload, me, me, spk)
|
||||
ChatMessageEvent echo = new ChatMessageEvent(
|
||||
uuid : uuid,
|
||||
payload : payload,
|
||||
sender : me,
|
||||
host : me,
|
||||
room : CONSOLE,
|
||||
chatTime : now,
|
||||
sig : sig
|
||||
)
|
||||
connections[d]?.sendChat(echo)
|
||||
}
|
||||
|
||||
private void processTrust(String shortName, TrustLevel level) {
|
||||
Persona p = shortNames.get(shortName)
|
||||
if (p == null)
|
||||
return
|
||||
eventBus.publish(new TrustEvent(persona : p, level : level))
|
||||
}
|
||||
|
||||
void stop() {
|
||||
if (running.compareAndSet(true, false)) {
|
||||
connections.each { k, v ->
|
||||
v.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,49 @@
|
||||
package com.muwire.core.chat
|
||||
|
||||
import java.util.concurrent.BlockingQueue
|
||||
import java.util.concurrent.LinkedBlockingQueue
|
||||
|
||||
import com.muwire.core.Persona
|
||||
|
||||
import groovy.util.logging.Log
|
||||
|
||||
@Log
|
||||
class LocalChatLink implements ChatLink {
|
||||
|
||||
public static final LocalChatLink INSTANCE = new LocalChatLink()
|
||||
|
||||
private final BlockingQueue messages = new LinkedBlockingQueue()
|
||||
|
||||
private LocalChatLink() {}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendChat(ChatMessageEvent e) {
|
||||
messages.put(e)
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendLeave(Persona p) {
|
||||
messages.put(p)
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendPing() {}
|
||||
|
||||
@Override
|
||||
public Object nextEvent() {
|
||||
messages.take()
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isUp() {
|
||||
true
|
||||
}
|
||||
|
||||
public Persona getPersona() {
|
||||
null
|
||||
}
|
||||
}
|
@@ -0,0 +1,8 @@
|
||||
package com.muwire.core.chat
|
||||
|
||||
import com.muwire.core.Event
|
||||
import com.muwire.core.Persona
|
||||
|
||||
class UIConnectChatEvent extends Event {
|
||||
Persona host
|
||||
}
|
@@ -0,0 +1,8 @@
|
||||
package com.muwire.core.chat
|
||||
|
||||
import com.muwire.core.Event
|
||||
import com.muwire.core.Persona
|
||||
|
||||
class UIDisconnectChatEvent extends Event {
|
||||
Persona host
|
||||
}
|
@@ -0,0 +1,9 @@
|
||||
package com.muwire.core.chat
|
||||
|
||||
import com.muwire.core.Event
|
||||
import com.muwire.core.Persona
|
||||
|
||||
class UserDisconnectedEvent extends Event {
|
||||
Persona user
|
||||
Persona host
|
||||
}
|
@@ -153,6 +153,10 @@ abstract class Connection implements Closeable {
|
||||
query.originator = e.originator.toBase64()
|
||||
if (e.sig != null)
|
||||
query.sig = Base64.encode(e.sig)
|
||||
if (e.queryTime > 0)
|
||||
query.queryTime = e.queryTime
|
||||
if (e.sig2 != null)
|
||||
query.sig2 = Base64.encode(e.sig2)
|
||||
messages.put(query)
|
||||
}
|
||||
|
||||
@@ -232,7 +236,6 @@ abstract class Connection implements Closeable {
|
||||
if (search.compressedResults != null)
|
||||
compressedResults = search.compressedResults
|
||||
byte[] sig = null
|
||||
// TODO: make this mandatory at some point
|
||||
if (search.sig != null) {
|
||||
sig = Base64.decode(search.sig)
|
||||
byte [] payload
|
||||
@@ -247,8 +250,36 @@ abstract class Connection implements Closeable {
|
||||
return
|
||||
} else
|
||||
log.info("query signature verified")
|
||||
} else
|
||||
} else {
|
||||
log.info("no signature in query")
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: make this mandatory at some point
|
||||
byte[] sig2 = null
|
||||
long queryTime = 0
|
||||
if (search.sig2 != null) {
|
||||
if (search.queryTime == null) {
|
||||
log.info("extended signature but no timestamp")
|
||||
return
|
||||
}
|
||||
sig2 = Base64.decode(search.sig2)
|
||||
queryTime = search.queryTime
|
||||
byte [] payload = (search.uuid + String.valueOf(queryTime)).getBytes(StandardCharsets.US_ASCII)
|
||||
def spk = originator.destination.getSigningPublicKey()
|
||||
def signature = new Signature(Constants.SIG_TYPE, sig2)
|
||||
if (!DSAEngine.getInstance().verifySignature(signature, payload, spk)) {
|
||||
log.info("extended signature didn't match uuid and timestamp")
|
||||
return
|
||||
} else {
|
||||
log.info("extended query signature verified")
|
||||
if (queryTime < System.currentTimeMillis() - Constants.MAX_QUERY_AGE) {
|
||||
log.info("query too old")
|
||||
return
|
||||
}
|
||||
}
|
||||
} else
|
||||
log.info("no extended signature in query")
|
||||
|
||||
SearchEvent searchEvent = new SearchEvent(searchTerms : search.keywords,
|
||||
searchHash : infohash,
|
||||
@@ -262,7 +293,9 @@ abstract class Connection implements Closeable {
|
||||
originator : originator,
|
||||
receivedOn : endpoint.destination,
|
||||
firstHop : search.firstHop,
|
||||
sig : sig )
|
||||
sig : sig,
|
||||
queryTime : queryTime,
|
||||
sig2 : sig2 )
|
||||
eventBus.publish(event)
|
||||
|
||||
}
|
||||
|
@@ -15,6 +15,7 @@ import com.muwire.core.EventBus
|
||||
import com.muwire.core.InfoHash
|
||||
import com.muwire.core.MuWireSettings
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.chat.ChatServer
|
||||
import com.muwire.core.filecert.Certificate
|
||||
import com.muwire.core.filecert.CertificateManager
|
||||
import com.muwire.core.files.FileManager
|
||||
@@ -50,6 +51,7 @@ class ConnectionAcceptor {
|
||||
final FileManager fileManager
|
||||
final ConnectionEstablisher establisher
|
||||
final CertificateManager certificateManager
|
||||
final ChatServer chatServer
|
||||
|
||||
final ExecutorService acceptorThread
|
||||
final ExecutorService handshakerThreads
|
||||
@@ -61,7 +63,8 @@ class ConnectionAcceptor {
|
||||
ConnectionAcceptor(EventBus eventBus, UltrapeerConnectionManager manager,
|
||||
MuWireSettings settings, I2PAcceptor acceptor, HostCache hostCache,
|
||||
TrustService trustService, SearchManager searchManager, UploadManager uploadManager,
|
||||
FileManager fileManager, ConnectionEstablisher establisher, CertificateManager certificateManager) {
|
||||
FileManager fileManager, ConnectionEstablisher establisher, CertificateManager certificateManager,
|
||||
ChatServer chatServer) {
|
||||
this.eventBus = eventBus
|
||||
this.manager = manager
|
||||
this.settings = settings
|
||||
@@ -73,6 +76,7 @@ class ConnectionAcceptor {
|
||||
this.uploadManager = uploadManager
|
||||
this.establisher = establisher
|
||||
this.certificateManager = certificateManager
|
||||
this.chatServer = chatServer
|
||||
|
||||
acceptorThread = Executors.newSingleThreadExecutor { r ->
|
||||
def rv = new Thread(r)
|
||||
@@ -154,12 +158,17 @@ class ConnectionAcceptor {
|
||||
case (byte)'C':
|
||||
processCERTIFICATES(e)
|
||||
break
|
||||
case (byte)'I':
|
||||
processIRC(e)
|
||||
break
|
||||
default:
|
||||
throw new Exception("Invalid read $read")
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
log.log(Level.WARNING, "incoming connection failed",ex)
|
||||
e.getOutputStream().close()
|
||||
try {
|
||||
e.getOutputStream().close()
|
||||
} catch (Exception ignore) {}
|
||||
e.close()
|
||||
eventBus.publish new ConnectionEvent(endpoint: e, incoming: true, leaf: null, status: ConnectionAttemptStatus.FAILED)
|
||||
}
|
||||
@@ -208,7 +217,9 @@ class ConnectionAcceptor {
|
||||
os.writeShort(json.bytes.length)
|
||||
os.write(json.bytes)
|
||||
}
|
||||
e.outputStream.close()
|
||||
try {
|
||||
e.outputStream.close()
|
||||
} catch (Exception ignored) {}
|
||||
e.close()
|
||||
eventBus.publish(new ConnectionEvent(endpoint: e, incoming: true, leaf: leaf, status: ConnectionAttemptStatus.REJECTED))
|
||||
}
|
||||
@@ -295,6 +306,10 @@ class ConnectionAcceptor {
|
||||
throw new IOException("No Sender header")
|
||||
if (!headers.containsKey("Count"))
|
||||
throw new IOException("No Count header")
|
||||
|
||||
boolean chat = false
|
||||
if (headers.containsKey('Chat'))
|
||||
chat = Boolean.parseBoolean(headers['Chat'])
|
||||
|
||||
byte [] personaBytes = Base64.decode(headers['Sender'])
|
||||
Persona sender = new Persona(new ByteArrayInputStream(personaBytes))
|
||||
@@ -313,6 +328,7 @@ class ConnectionAcceptor {
|
||||
dis.readFully(payload)
|
||||
def json = slurper.parse(payload)
|
||||
results[i] = ResultsParser.parse(sender, resultsUUID, json)
|
||||
results[i].chat = chat
|
||||
}
|
||||
eventBus.publish(new UIResultBatchEvent(uuid: resultsUUID, results: results))
|
||||
} catch (IOException bad) {
|
||||
@@ -379,10 +395,10 @@ class ConnectionAcceptor {
|
||||
dis.readFully(RUST)
|
||||
if (RUST != "RUST\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
throw new IOException("Invalid TRUST connection")
|
||||
String header
|
||||
while ((header = DataUtil.readTillRN(dis)) != ""); // ignore headers for now
|
||||
|
||||
OutputStream os = e.getOutputStream()
|
||||
|
||||
Map<String,String> headers = DataUtil.readAllHeaders(dis)
|
||||
|
||||
OutputStream os = e.getOutputStream()
|
||||
if (!settings.allowTrustLists) {
|
||||
os.write("403 Not Allowed\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
os.flush()
|
||||
@@ -390,22 +406,53 @@ class ConnectionAcceptor {
|
||||
return
|
||||
}
|
||||
|
||||
os.write("200 OK\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
List<Persona> good = new ArrayList<>(trustService.good.values())
|
||||
int size = Math.min(Short.MAX_VALUE * 2, good.size())
|
||||
good = good.subList(0, size)
|
||||
os.write("200 OK\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
|
||||
boolean json = headers.containsKey('Json') && Boolean.parseBoolean(headers['Json'])
|
||||
|
||||
List<TrustService.TrustEntry> good = new ArrayList<>(trustService.good.values())
|
||||
List<TrustService.TrustEntry> bad = new ArrayList<>(trustService.bad.values())
|
||||
DataOutputStream dos = new DataOutputStream(os)
|
||||
dos.writeShort(size)
|
||||
good.each {
|
||||
it.write(dos)
|
||||
}
|
||||
|
||||
List<Persona> bad = new ArrayList<>(trustService.bad.values())
|
||||
size = Math.min(Short.MAX_VALUE * 2, bad.size())
|
||||
bad = bad.subList(0, size)
|
||||
dos.writeShort(size)
|
||||
bad.each {
|
||||
it.write(dos)
|
||||
if (!json) {
|
||||
os.write("\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
int size = Math.min(Short.MAX_VALUE * 2, good.size())
|
||||
good = good.subList(0, size)
|
||||
dos.writeShort(size)
|
||||
good.each {
|
||||
it.persona.write(dos)
|
||||
}
|
||||
|
||||
size = Math.min(Short.MAX_VALUE * 2, bad.size())
|
||||
bad = bad.subList(0, size)
|
||||
dos.writeShort(size)
|
||||
bad.each {
|
||||
it.persona.write(dos)
|
||||
}
|
||||
} else {
|
||||
dos.write("Json: true\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
dos.write("Good:${good.size()}\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
dos.write("Bad:${bad.size()}\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
dos.write("\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
|
||||
good.each {
|
||||
def obj = [:]
|
||||
obj.persona = it.persona.toBase64()
|
||||
obj.reason = it.reason
|
||||
String toJson = JsonOutput.toJson(obj)
|
||||
byte [] payload = toJson.getBytes(StandardCharsets.US_ASCII)
|
||||
dos.writeShort(payload.length)
|
||||
dos.write(payload)
|
||||
}
|
||||
bad.each {
|
||||
def obj = [:]
|
||||
obj.persona = it.persona.toBase64()
|
||||
obj.reason = it.reason
|
||||
String toJson = JsonOutput.toJson(obj)
|
||||
byte [] payload = toJson.getBytes(StandardCharsets.US_ASCII)
|
||||
dos.writeShort(payload.length)
|
||||
dos.write(payload)
|
||||
}
|
||||
}
|
||||
|
||||
dos.flush()
|
||||
@@ -463,5 +510,14 @@ class ConnectionAcceptor {
|
||||
e.close()
|
||||
}
|
||||
}
|
||||
|
||||
private void processIRC(Endpoint e) {
|
||||
byte[] IRC = new byte[4]
|
||||
DataInputStream dis = new DataInputStream(e.getInputStream())
|
||||
dis.readFully(IRC)
|
||||
if (IRC != "RC\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
throw new Exception("Invalid IRC connection")
|
||||
chatServer.handle(e)
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -276,6 +276,57 @@ public class Downloader {
|
||||
activeWorkers.put(d, newWorker)
|
||||
executorService.submit(newWorker)
|
||||
}
|
||||
|
||||
boolean isSequential() {
|
||||
pieces.ratio == 0f
|
||||
}
|
||||
|
||||
File generatePreview() {
|
||||
int lastCompletePiece = pieces.firstIncomplete() - 1
|
||||
if (lastCompletePiece == -1)
|
||||
return null
|
||||
if (lastCompletePiece < -1)
|
||||
return file
|
||||
long previewableLength = (lastCompletePiece + 1) * ((long)pieceSize)
|
||||
|
||||
// generate name
|
||||
long now = System.currentTimeMillis()
|
||||
File previewFile
|
||||
File parentFile = file.getParentFile()
|
||||
int lastDot = file.getName().lastIndexOf('.')
|
||||
if (lastDot < 0)
|
||||
previewFile = new File(parentFile, file.getName() + "." + String.valueOf(now) + ".mwpreview")
|
||||
else {
|
||||
String name = file.getName().substring(0, lastDot)
|
||||
String extension = file.getName().substring(lastDot + 1)
|
||||
String previewName = name + "." + String.valueOf(now) + ".mwpreview."+extension
|
||||
previewFile = new File(parentFile, previewName)
|
||||
}
|
||||
|
||||
// copy
|
||||
InputStream is = null
|
||||
OutputStream os = null
|
||||
try {
|
||||
is = new BufferedInputStream(new FileInputStream(incompleteFile))
|
||||
os = new BufferedOutputStream(new FileOutputStream(previewFile))
|
||||
byte [] tmp = new byte[0x1 << 13]
|
||||
long totalCopied = 0
|
||||
while(totalCopied < previewableLength) {
|
||||
int read = is.read(tmp, 0, (int)Math.min(tmp.length, previewableLength - totalCopied))
|
||||
if (read < 0)
|
||||
throw new IOException("EOF?")
|
||||
os.write(tmp, 0, read)
|
||||
totalCopied += read
|
||||
}
|
||||
return previewFile
|
||||
} catch (IOException bad) {
|
||||
log.log(Level.WARNING,"Preview failed",bad)
|
||||
return null
|
||||
} finally {
|
||||
try {is?.close() } catch (IOException ignore) {}
|
||||
try {os?.close() } catch (IOException ignore) {}
|
||||
}
|
||||
}
|
||||
|
||||
class DownloadWorker implements Runnable {
|
||||
private final Destination destination
|
||||
|
@@ -108,6 +108,10 @@ class Pieces {
|
||||
partials.clear()
|
||||
}
|
||||
|
||||
synchronized int firstIncomplete() {
|
||||
done.nextClearBit(0)
|
||||
}
|
||||
|
||||
synchronized void write(PrintWriter writer) {
|
||||
for (int i = done.nextSetBit(0); i >= 0; i = done.nextSetBit(i+1)) {
|
||||
writer.println(i)
|
||||
|
@@ -143,6 +143,7 @@ class FileManager {
|
||||
|
||||
String comment = sf.getComment()
|
||||
if (comment != null) {
|
||||
comment = DataUtil.readi18nString(Base64.decode(comment))
|
||||
Set<File> existingComment = commentToFile.get(comment)
|
||||
if (existingComment != null) {
|
||||
existingComment.remove(sf.getFile())
|
||||
@@ -229,7 +230,7 @@ class FileManager {
|
||||
return files
|
||||
Set<SharedFile> rv = new HashSet<>()
|
||||
files.each {
|
||||
if (it.getPieceSize() != 0)
|
||||
if (it != null && it.getPieceSize() != 0)
|
||||
rv.add(it)
|
||||
}
|
||||
rv
|
||||
|
@@ -7,7 +7,7 @@ class FileTree {
|
||||
private final TreeNode root = new TreeNode()
|
||||
private final Map<File, TreeNode> fileToNode = new ConcurrentHashMap<>()
|
||||
|
||||
void add(File file) {
|
||||
synchronized void add(File file) {
|
||||
List<File> path = new ArrayList<>()
|
||||
path.add(file)
|
||||
while (file.getParentFile() != null) {
|
||||
@@ -31,7 +31,7 @@ class FileTree {
|
||||
}
|
||||
}
|
||||
|
||||
boolean remove(File file) {
|
||||
synchronized boolean remove(File file) {
|
||||
TreeNode node = fileToNode.remove(file)
|
||||
if (node == null) {
|
||||
return false
|
||||
|
@@ -13,6 +13,8 @@ class QueryEvent extends Event {
|
||||
Persona originator
|
||||
Destination receivedOn
|
||||
byte[] sig
|
||||
long queryTime
|
||||
byte[] sig2
|
||||
|
||||
String toString() {
|
||||
"searchEvent: $searchEvent firstHop:$firstHop, replyTo:${replyTo.toBase32()}" +
|
||||
|
@@ -1,6 +1,7 @@
|
||||
package com.muwire.core.search
|
||||
|
||||
import com.muwire.core.SharedFile
|
||||
import com.muwire.core.chat.ChatServer
|
||||
import com.muwire.core.connection.Endpoint
|
||||
import com.muwire.core.connection.I2PConnector
|
||||
import com.muwire.core.filecert.CertificateManager
|
||||
@@ -48,13 +49,16 @@ class ResultsSender {
|
||||
private final EventBus eventBus
|
||||
private final MuWireSettings settings
|
||||
private final CertificateManager certificateManager
|
||||
private final ChatServer chatServer
|
||||
|
||||
ResultsSender(EventBus eventBus, I2PConnector connector, Persona me, MuWireSettings settings, CertificateManager certificateManager) {
|
||||
ResultsSender(EventBus eventBus, I2PConnector connector, Persona me, MuWireSettings settings,
|
||||
CertificateManager certificateManager, ChatServer chatServer) {
|
||||
this.connector = connector;
|
||||
this.eventBus = eventBus
|
||||
this.me = me
|
||||
this.settings = settings
|
||||
this.certificateManager = certificateManager
|
||||
this.chatServer = chatServer
|
||||
}
|
||||
|
||||
void sendResults(UUID uuid, SharedFile[] results, Destination target, boolean oobInfohash, boolean compressedResults) {
|
||||
@@ -80,9 +84,11 @@ class ResultsSender {
|
||||
infohash : it.getInfoHash(),
|
||||
pieceSize : pieceSize,
|
||||
uuid : uuid,
|
||||
browse : settings.browseFiles,
|
||||
sources : suggested,
|
||||
comment : comment,
|
||||
certificates : certificates
|
||||
certificates : certificates,
|
||||
chat : chatServer.running.get() && settings.advertiseChat
|
||||
)
|
||||
uiResultEvents << uiResultEvent
|
||||
}
|
||||
@@ -130,6 +136,8 @@ class ResultsSender {
|
||||
os.write("RESULTS $uuid\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
os.write("Sender: ${me.toBase64()}\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
os.write("Count: $results.length\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
boolean chat = chatServer.running.get() && settings.advertiseChat
|
||||
os.write("Chat: $chat\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
os.write("\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
DataOutputStream dos = new DataOutputStream(new GZIPOutputStream(os))
|
||||
results.each {
|
||||
|
@@ -39,10 +39,11 @@ class SearchIndex {
|
||||
split.each { if (it.length() > 0) rv << it }
|
||||
|
||||
// then just by ' '
|
||||
source.split(' ').each { if (it.length() > 0) rv << it }
|
||||
source.toLowerCase().split(' ').each { if (it.length() > 0) rv << it }
|
||||
|
||||
// and add original string
|
||||
rv << source
|
||||
rv << source.toLowerCase()
|
||||
rv.toArray(new String[0])
|
||||
}
|
||||
|
||||
|
@@ -17,6 +17,7 @@ class UIResultEvent extends Event {
|
||||
String comment
|
||||
boolean browse
|
||||
int certificates
|
||||
boolean chat
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
|
@@ -3,6 +3,7 @@ package com.muwire.core.trust
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.trust.TrustService.TrustEntry
|
||||
|
||||
import net.i2p.util.ConcurrentHashSet
|
||||
|
||||
@@ -10,7 +11,7 @@ class RemoteTrustList {
|
||||
public enum Status { NEW, UPDATING, UPDATED, UPDATE_FAILED }
|
||||
|
||||
private final Persona persona
|
||||
private final Set<Persona> good, bad
|
||||
private final Set<TrustEntry> good, bad
|
||||
volatile long timestamp
|
||||
volatile boolean forceUpdate
|
||||
Status status = Status.NEW
|
||||
|
@@ -7,4 +7,5 @@ class TrustEvent extends Event {
|
||||
|
||||
Persona persona
|
||||
TrustLevel level
|
||||
String reason
|
||||
}
|
||||
|
@@ -1,21 +1,26 @@
|
||||
package com.muwire.core.trust
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.logging.Level
|
||||
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.Service
|
||||
|
||||
import groovy.json.JsonOutput
|
||||
import groovy.json.JsonSlurper
|
||||
import groovy.util.logging.Log
|
||||
import net.i2p.data.Base64
|
||||
import net.i2p.data.Destination
|
||||
import net.i2p.util.ConcurrentHashSet
|
||||
|
||||
@Log
|
||||
class TrustService extends Service {
|
||||
|
||||
final File persistGood, persistBad
|
||||
final long persistInterval
|
||||
|
||||
final Map<Destination, Persona> good = new ConcurrentHashMap<>()
|
||||
final Map<Destination, Persona> bad = new ConcurrentHashMap<>()
|
||||
final Map<Destination, TrustEntry> good = new ConcurrentHashMap<>()
|
||||
final Map<Destination, TrustEntry> bad = new ConcurrentHashMap<>()
|
||||
|
||||
final Timer timer
|
||||
|
||||
@@ -37,18 +42,41 @@ class TrustService extends Service {
|
||||
}
|
||||
|
||||
void load() {
|
||||
JsonSlurper slurper = new JsonSlurper()
|
||||
if (persistGood.exists()) {
|
||||
persistGood.eachLine {
|
||||
byte [] decoded = Base64.decode(it)
|
||||
Persona persona = new Persona(new ByteArrayInputStream(decoded))
|
||||
good.put(persona.destination, persona)
|
||||
try {
|
||||
byte [] decoded = Base64.decode(it)
|
||||
Persona persona = new Persona(new ByteArrayInputStream(decoded))
|
||||
good.put(persona.destination, new TrustEntry(persona, null))
|
||||
} catch (Exception e) {
|
||||
try {
|
||||
def json = slurper.parseText(it)
|
||||
byte [] decoded = Base64.decode(json.persona)
|
||||
Persona persona = new Persona(new ByteArrayInputStream(decoded))
|
||||
good.put(persona.destination, new TrustEntry(persona, json.reason))
|
||||
} catch (Exception bad) {
|
||||
log.log(Level.WARNING,"couldn't parse trust entry $it",bad)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (persistBad.exists()) {
|
||||
persistBad.eachLine {
|
||||
byte [] decoded = Base64.decode(it)
|
||||
Persona persona = new Persona(new ByteArrayInputStream(decoded))
|
||||
bad.put(persona.destination, persona)
|
||||
try {
|
||||
byte [] decoded = Base64.decode(it)
|
||||
Persona persona = new Persona(new ByteArrayInputStream(decoded))
|
||||
bad.put(persona.destination, new TrustEntry(persona, null))
|
||||
} catch (Exception e) {
|
||||
try {
|
||||
def json = slurper.parseText(it)
|
||||
byte [] decoded = Base64.decode(json.persona)
|
||||
Persona persona = new Persona(new ByteArrayInputStream(decoded))
|
||||
bad.put(persona.destination, new TrustEntry(persona, json.reason))
|
||||
} catch (Exception bad) {
|
||||
log.log(Level.WARNING,"couldn't parse trust entry $it",bad)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
timer.schedule({persist()} as TimerTask, persistInterval, persistInterval)
|
||||
@@ -59,13 +87,19 @@ class TrustService extends Service {
|
||||
persistGood.delete()
|
||||
persistGood.withPrintWriter { writer ->
|
||||
good.each {k,v ->
|
||||
writer.println v.toBase64()
|
||||
def json = [:]
|
||||
json.persona = v.persona.toBase64()
|
||||
json.reason = v.reason
|
||||
writer.println JsonOutput.toJson(json)
|
||||
}
|
||||
}
|
||||
persistBad.delete()
|
||||
persistBad.withPrintWriter { writer ->
|
||||
bad.each { k,v ->
|
||||
writer.println v.toBase64()
|
||||
def json = [:]
|
||||
json.persona = v.persona.toBase64()
|
||||
json.reason = v.reason
|
||||
writer.println JsonOutput.toJson(json)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -82,11 +116,11 @@ class TrustService extends Service {
|
||||
switch(e.level) {
|
||||
case TrustLevel.TRUSTED:
|
||||
bad.remove(e.persona.destination)
|
||||
good.put(e.persona.destination, e.persona)
|
||||
good.put(e.persona.destination, new TrustEntry(e.persona, e.reason))
|
||||
break
|
||||
case TrustLevel.DISTRUSTED:
|
||||
good.remove(e.persona.destination)
|
||||
bad.put(e.persona.destination, e.persona)
|
||||
bad.put(e.persona.destination, new TrustEntry(e.persona, e.reason))
|
||||
break
|
||||
case TrustLevel.NEUTRAL:
|
||||
good.remove(e.persona.destination)
|
||||
@@ -94,4 +128,24 @@ class TrustService extends Service {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
public static class TrustEntry {
|
||||
private final Persona persona
|
||||
private final String reason
|
||||
TrustEntry(Persona persona, String reason) {
|
||||
this.persona = persona
|
||||
this.reason = reason
|
||||
}
|
||||
|
||||
public int hashCode() {
|
||||
persona.hashCode()
|
||||
}
|
||||
|
||||
public boolean equals(Object o) {
|
||||
if (!(o instanceof TrustEntry))
|
||||
return false
|
||||
persona == o.persona
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -12,9 +12,12 @@ import com.muwire.core.Persona
|
||||
import com.muwire.core.UILoadedEvent
|
||||
import com.muwire.core.connection.Endpoint
|
||||
import com.muwire.core.connection.I2PConnector
|
||||
import com.muwire.core.trust.TrustService.TrustEntry
|
||||
import com.muwire.core.util.DataUtil
|
||||
|
||||
import groovy.json.JsonSlurper
|
||||
import groovy.util.logging.Log
|
||||
import net.i2p.data.Base64
|
||||
import net.i2p.data.Destination
|
||||
|
||||
@Log
|
||||
@@ -109,7 +112,9 @@ class TrustSubscriber {
|
||||
endpoint = i2pConnector.connect(trustList.persona.destination)
|
||||
OutputStream os = endpoint.getOutputStream()
|
||||
InputStream is = endpoint.getInputStream()
|
||||
os.write("TRUST\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
os.write("TRUST\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
os.write("Json:true\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
os.write("\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
os.flush()
|
||||
|
||||
String codeString = DataUtil.readTillRN(is)
|
||||
@@ -123,24 +128,47 @@ class TrustSubscriber {
|
||||
return false
|
||||
}
|
||||
|
||||
// swallow any headers
|
||||
String header
|
||||
while (( header = DataUtil.readTillRN(is)) != "");
|
||||
|
||||
Map<String,String> headers = DataUtil.readAllHeaders(is)
|
||||
DataInputStream dis = new DataInputStream(is)
|
||||
Set<TrustService.TrustEntry> good = new HashSet<>()
|
||||
Set<TrustService.TrustEntry> bad = new HashSet<>()
|
||||
|
||||
if (headers.containsKey('Json') && Boolean.parseBoolean(headers['Json'])) {
|
||||
int countGood = Integer.parseInt(headers['Good'])
|
||||
int countBad = Integer.parseInt(headers['Bad'])
|
||||
|
||||
JsonSlurper slurper = new JsonSlurper()
|
||||
|
||||
for (int i = 0; i < countGood; i++) {
|
||||
int length = dis.readUnsignedShort()
|
||||
byte []payload = new byte[length]
|
||||
dis.readFully(payload)
|
||||
def json = slurper.parse(payload)
|
||||
Persona persona = new Persona(new ByteArrayInputStream(Base64.decode(json.persona)))
|
||||
good.add(new TrustEntry(persona, json.reason))
|
||||
}
|
||||
|
||||
for (int i = 0; i < countBad; i++) {
|
||||
int length = dis.readUnsignedShort()
|
||||
byte []payload = new byte[length]
|
||||
dis.readFully(payload)
|
||||
def json = slurper.parse(payload)
|
||||
Persona persona = new Persona(new ByteArrayInputStream(Base64.decode(json.persona)))
|
||||
bad.add(new TrustEntry(persona, json.reason))
|
||||
}
|
||||
|
||||
} else {
|
||||
int nGood = dis.readUnsignedShort()
|
||||
for (int i = 0; i < nGood; i++) {
|
||||
Persona p = new Persona(dis)
|
||||
good.add(new TrustEntry(p,null))
|
||||
}
|
||||
|
||||
Set<Persona> good = new HashSet<>()
|
||||
int nGood = dis.readUnsignedShort()
|
||||
for (int i = 0; i < nGood; i++) {
|
||||
Persona p = new Persona(dis)
|
||||
good.add(p)
|
||||
}
|
||||
|
||||
Set<Persona> bad = new HashSet<>()
|
||||
int nBad = dis.readUnsignedShort()
|
||||
for (int i = 0; i < nBad; i++) {
|
||||
Persona p = new Persona(dis)
|
||||
bad.add(p)
|
||||
int nBad = dis.readUnsignedShort()
|
||||
for (int i = 0; i < nBad; i++) {
|
||||
Persona p = new Persona(dis)
|
||||
bad.add(new TrustEntry(p, null))
|
||||
}
|
||||
}
|
||||
|
||||
trustList.timestamp = now
|
||||
|
@@ -13,6 +13,7 @@ import com.muwire.core.files.FileSharedEvent
|
||||
import com.muwire.core.search.QueryEvent
|
||||
import com.muwire.core.search.SearchEvent
|
||||
import com.muwire.core.search.UIResultBatchEvent
|
||||
import com.muwire.core.util.DataUtil
|
||||
|
||||
import groovy.json.JsonOutput
|
||||
import groovy.json.JsonSlurper
|
||||
@@ -176,9 +177,12 @@ class UpdateClient {
|
||||
signer = payload.signer
|
||||
log.info("starting search for new version hash $payload.infoHash")
|
||||
Signature sig = DSAEngine.getInstance().sign(updateInfoHash.getRoot(), spk)
|
||||
def searchEvent = new SearchEvent(searchHash : updateInfoHash.getRoot(), uuid : UUID.randomUUID(), oobInfohash : true, persona : me)
|
||||
UUID uuid = UUID.randomUUID()
|
||||
long timestamp = System.currentTimeMillis()
|
||||
byte [] sig2 = DataUtil.signUUID(uuid, timestamp, spk)
|
||||
def searchEvent = new SearchEvent(searchHash : updateInfoHash.getRoot(), uuid : uuid, oobInfohash : true, persona : me)
|
||||
def queryEvent = new QueryEvent(searchEvent : searchEvent, firstHop : true, replyTo : me.destination,
|
||||
receivedOn : me.destination, originator : me, sig : sig.data)
|
||||
receivedOn : me.destination, originator : me, sig : sig.data, queryTime : timestamp, sig2 : sig2)
|
||||
eventBus.publish(queryEvent)
|
||||
}
|
||||
}
|
||||
|
@@ -5,6 +5,8 @@ import net.i2p.crypto.SigType;
|
||||
public class Constants {
|
||||
public static final byte PERSONA_VERSION = (byte)1;
|
||||
public static final byte FILE_CERT_VERSION = (byte)2;
|
||||
public static final int CHAT_VERSION = 1;
|
||||
|
||||
public static final SigType SIG_TYPE = SigType.EdDSA_SHA512_Ed25519;
|
||||
|
||||
public static final int MAX_HEADER_SIZE = 0x1 << 14;
|
||||
@@ -13,4 +15,6 @@ public class Constants {
|
||||
public static final int MAX_RESULTS = 0x1 << 16;
|
||||
|
||||
public static final int MAX_COMMENT_LENGTH = 0x1 << 15;
|
||||
|
||||
public static final long MAX_QUERY_AGE = 5 * 60 * 1000L;
|
||||
}
|
||||
|
@@ -109,6 +109,10 @@ public class SharedFile {
|
||||
return downloaders;
|
||||
}
|
||||
|
||||
public Set<SearchEntry> getSearches() {
|
||||
return searches;
|
||||
}
|
||||
|
||||
public void addDownloader(String name) {
|
||||
downloaders.add(name);
|
||||
}
|
||||
|
@@ -15,11 +15,15 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.muwire.core.Constants;
|
||||
|
||||
import net.i2p.crypto.DSAEngine;
|
||||
import net.i2p.data.Base64;
|
||||
import net.i2p.data.Signature;
|
||||
import net.i2p.data.SigningPrivateKey;
|
||||
import net.i2p.util.ConcurrentHashSet;
|
||||
|
||||
public class DataUtil {
|
||||
@@ -203,4 +207,10 @@ public class DataUtil {
|
||||
.collect(Collectors.joining(","));
|
||||
props.setProperty(property, encoded);
|
||||
}
|
||||
|
||||
public static byte[] signUUID(UUID uuid, long timestamp, SigningPrivateKey spk) {
|
||||
byte [] payload = (uuid.toString() + String.valueOf(timestamp)).getBytes(StandardCharsets.US_ASCII);
|
||||
Signature sig = DSAEngine.getInstance().sign(payload, spk);
|
||||
return sig.getData();
|
||||
}
|
||||
}
|
||||
|
@@ -1,5 +1,7 @@
|
||||
package com.muwire.core.files
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertAll
|
||||
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
@@ -9,6 +11,9 @@ import com.muwire.core.MuWireSettings
|
||||
import com.muwire.core.SharedFile
|
||||
import com.muwire.core.search.ResultsEvent
|
||||
import com.muwire.core.search.SearchEvent
|
||||
import com.muwire.core.util.DataUtil
|
||||
|
||||
import net.i2p.data.Base64
|
||||
|
||||
class FileManagerTest {
|
||||
|
||||
@@ -185,4 +190,39 @@ class FileManagerTest {
|
||||
assert results == null
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
void testComplicatedScenario() {
|
||||
// this tries to reproduce an NPE when un-sharing then sharing again and searching
|
||||
String comment = "same comment"
|
||||
comment = Base64.encode(DataUtil.encodei18nString(comment))
|
||||
File f1 = new File("MuWire-0.5.10.AppImage")
|
||||
InfoHash ih1 = InfoHash.fromHashList(new byte[32])
|
||||
SharedFile sf1 = new SharedFile(f1, ih1, 0)
|
||||
sf1.setComment(comment)
|
||||
|
||||
manager.onFileLoadedEvent(new FileLoadedEvent(loadedFile : sf1))
|
||||
manager.onFileUnsharedEvent(new FileUnsharedEvent(unsharedFile : sf1, deleted : true))
|
||||
|
||||
File f2 = new File("MuWire-0.6.0.AppImage")
|
||||
InfoHash ih2 = InfoHash.fromHashList(new byte[64])
|
||||
SharedFile sf2 = new SharedFile(f2, ih2, 0)
|
||||
sf2.setComment(comment)
|
||||
|
||||
manager.onFileLoadedEvent(new FileLoadedEvent(loadedFile : sf2))
|
||||
|
||||
manager.onSearchEvent(new SearchEvent(searchTerms : ["muwire"]))
|
||||
Thread.sleep(20)
|
||||
|
||||
assert results != null
|
||||
assert results.results.size() == 1
|
||||
assert results.results.contains(sf2)
|
||||
|
||||
results = null
|
||||
manager.onSearchEvent(new SearchEvent(searchTerms : ['comment'], searchComments : true, oobInfohash : true))
|
||||
Thread.sleep(20)
|
||||
assert results != null
|
||||
assert results.results.size() == 1
|
||||
assert results.results.contains(sf2)
|
||||
}
|
||||
}
|
||||
|
@@ -8,6 +8,7 @@ import com.muwire.core.Destinations
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.Personas
|
||||
|
||||
import groovy.json.JsonSlurper
|
||||
import net.i2p.data.Base64
|
||||
import net.i2p.data.Destination
|
||||
|
||||
@@ -55,13 +56,16 @@ class TrustServiceTest {
|
||||
service.onTrustEvent new TrustEvent(level: TrustLevel.DISTRUSTED, persona: personas.persona2)
|
||||
|
||||
Thread.sleep(250)
|
||||
JsonSlurper slurper = new JsonSlurper()
|
||||
def trusted = new HashSet<>()
|
||||
persistGood.eachLine {
|
||||
trusted.add(new Persona(new ByteArrayInputStream(Base64.decode(it))))
|
||||
def json = slurper.parseText(it)
|
||||
trusted.add(new Persona(new ByteArrayInputStream(Base64.decode(json.persona))))
|
||||
}
|
||||
def distrusted = new HashSet<>()
|
||||
persistBad.eachLine {
|
||||
distrusted.add(new Persona(new ByteArrayInputStream(Base64.decode(it))))
|
||||
def json = slurper.parseText(it)
|
||||
distrusted.add(new Persona(new ByteArrayInputStream(Base64.decode(json.persona))))
|
||||
}
|
||||
|
||||
assert trusted.size() == 1
|
||||
|
@@ -1,5 +1,5 @@
|
||||
group = com.muwire
|
||||
version = 0.5.9
|
||||
version = 0.6.6
|
||||
i2pVersion = 0.9.43
|
||||
groovyVersion = 2.4.15
|
||||
slf4jVersion = 1.7.25
|
||||
|
@@ -9,7 +9,7 @@ buildscript {
|
||||
classpath 'org.kt3k.gradle.plugin:coveralls-gradle-plugin:2.8.2'
|
||||
classpath 'nl.javadude.gradle.plugins:license-gradle-plugin:0.11.0'
|
||||
classpath 'org.gradle.api.plugins:gradle-izpack-plugin:0.2.3'
|
||||
classpath 'com.github.jengelman.gradle.plugins:shadow:2.0.4'
|
||||
classpath 'com.github.jengelman.gradle.plugins:shadow:5.2.0'
|
||||
classpath 'com.github.cr0:gradle-macappbundle-plugin:3.1.0'
|
||||
classpath 'org.kordamp.gradle:stats-gradle-plugin:0.2.2'
|
||||
classpath 'com.github.ben-manes:gradle-versions-plugin:0.17.0'
|
||||
|
@@ -106,4 +106,19 @@ mvcGroups {
|
||||
view = 'com.muwire.gui.SharedFileView'
|
||||
controller = 'com.muwire.gui.SharedFileController'
|
||||
}
|
||||
'download-preview' {
|
||||
model = "com.muwire.gui.DownloadPreviewModel"
|
||||
view = "com.muwire.gui.DownloadPreviewView"
|
||||
controller = "com.muwire.gui.DownloadPreviewController"
|
||||
}
|
||||
'chat-server' {
|
||||
model = 'com.muwire.gui.ChatServerModel'
|
||||
view = 'com.muwire.gui.ChatServerView'
|
||||
controller = 'com.muwire.gui.ChatServerController'
|
||||
}
|
||||
'chat-room' {
|
||||
model = 'com.muwire.gui.ChatRoomModel'
|
||||
view = 'com.muwire.gui.ChatRoomView'
|
||||
controller = 'com.muwire.gui.ChatRoomController'
|
||||
}
|
||||
}
|
||||
|
@@ -64,8 +64,11 @@ class BrowseController {
|
||||
def selectedResults = view.selectedResults()
|
||||
if (selectedResults == null || selectedResults.isEmpty())
|
||||
return
|
||||
|
||||
def group = application.mvcGroupManager.getGroups()['MainFrame']
|
||||
|
||||
selectedResults.removeAll {
|
||||
!mvcGroup.parentGroup.parentGroup.model.canDownload(it.infohash)
|
||||
!group.model.canDownload(it.infohash)
|
||||
}
|
||||
|
||||
selectedResults.each { result ->
|
||||
@@ -74,11 +77,11 @@ class BrowseController {
|
||||
result : [result],
|
||||
sources : [model.host.destination],
|
||||
target : file,
|
||||
sequential : mvcGroup.parentGroup.view.sequentialDownloadCheckbox.model.isSelected()
|
||||
sequential : view.sequentialDownloadCheckbox.model.isSelected()
|
||||
))
|
||||
}
|
||||
|
||||
mvcGroup.parentGroup.parentGroup.view.showDownloadsWindow.call()
|
||||
group.view.showDownloadsWindow.call()
|
||||
dismiss()
|
||||
}
|
||||
|
||||
|
@@ -0,0 +1,274 @@
|
||||
package com.muwire.gui
|
||||
|
||||
import griffon.core.artifact.GriffonController
|
||||
import griffon.core.controller.ControllerAction
|
||||
import griffon.inject.MVCMember
|
||||
import griffon.metadata.ArtifactProviderFor
|
||||
import groovy.util.logging.Log
|
||||
import net.i2p.crypto.DSAEngine
|
||||
import net.i2p.data.Base64
|
||||
import net.i2p.data.DataHelper
|
||||
import net.i2p.data.Signature
|
||||
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util.logging.Level
|
||||
|
||||
import javax.annotation.Nonnull
|
||||
import javax.swing.JOptionPane
|
||||
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.chat.ChatCommand
|
||||
import com.muwire.core.chat.ChatAction
|
||||
import com.muwire.core.chat.ChatConnection
|
||||
import com.muwire.core.chat.ChatMessageEvent
|
||||
import com.muwire.core.chat.ChatServer
|
||||
import com.muwire.core.trust.TrustEvent
|
||||
import com.muwire.core.trust.TrustLevel
|
||||
|
||||
@Log
|
||||
@ArtifactProviderFor(GriffonController)
|
||||
class ChatRoomController {
|
||||
@MVCMember @Nonnull
|
||||
ChatRoomModel model
|
||||
@MVCMember @Nonnull
|
||||
ChatRoomView view
|
||||
|
||||
boolean leftRoom
|
||||
|
||||
@ControllerAction
|
||||
void say() {
|
||||
String words = view.sayField.text
|
||||
view.sayField.setText(null)
|
||||
|
||||
ChatCommand command
|
||||
try {
|
||||
command = new ChatCommand(words)
|
||||
} catch (Exception nope) {
|
||||
command = new ChatCommand("/SAY $words")
|
||||
}
|
||||
|
||||
if (!command.action.user) {
|
||||
JOptionPane.showMessageDialog(null, "$words is not a user command","Invalid Command", JOptionPane.ERROR_MESSAGE)
|
||||
return
|
||||
}
|
||||
long now = System.currentTimeMillis()
|
||||
|
||||
if (command.action == ChatAction.SAY && command.payload.length() > 0) {
|
||||
String toShow = DataHelper.formatTime(now) + " <" + model.core.me.getHumanReadableName() + "> "+command.payload
|
||||
|
||||
view.roomTextArea.append(toShow)
|
||||
view.roomTextArea.append('\n')
|
||||
trimLines()
|
||||
}
|
||||
|
||||
if (command.action == ChatAction.JOIN) {
|
||||
String newRoom = command.payload
|
||||
if (!mvcGroup.parentGroup.childrenGroups.containsKey(newRoom)) {
|
||||
def params = [:]
|
||||
params['core'] = model.core
|
||||
params['tabName'] = model.host.getHumanReadableName() + "-chat-rooms"
|
||||
params['room'] = newRoom
|
||||
params['console'] = false
|
||||
params['host'] = model.host
|
||||
params['roomTabName'] = newRoom
|
||||
params['chatNotificator'] = view.chatNotificator
|
||||
|
||||
mvcGroup.parentGroup.createMVCGroup("chat-room", model.host.getHumanReadableName()+"-"+newRoom, params)
|
||||
}
|
||||
}
|
||||
if (command.action == ChatAction.LEAVE && !model.console) {
|
||||
leftRoom = true
|
||||
view.closeTab.call()
|
||||
}
|
||||
|
||||
String room = model.console ? ChatServer.CONSOLE : model.room
|
||||
|
||||
UUID uuid = UUID.randomUUID()
|
||||
byte [] sig = ChatConnection.sign(uuid, now, room, command.source, model.core.me, model.host, model.core.spk)
|
||||
|
||||
def event = new ChatMessageEvent(uuid : uuid,
|
||||
payload : command.source,
|
||||
sender : model.core.me,
|
||||
host : model.host,
|
||||
room : room,
|
||||
chatTime : now,
|
||||
sig : sig)
|
||||
|
||||
model.core.eventBus.publish(event)
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void privateMessage() {
|
||||
Persona p = view.getSelectedPersona()
|
||||
if (p == null)
|
||||
return
|
||||
String groupId = model.host.getHumanReadableName() + "-" + p.getHumanReadableName() +"-private-chat"
|
||||
if (p != model.core.me && !mvcGroup.parentGroup.childrenGroups.containsKey(groupId)) {
|
||||
def params = [:]
|
||||
params['core'] = model.core
|
||||
params['tabName'] = model.tabName
|
||||
params['room'] = p.toBase64()
|
||||
params['privateChat'] = true
|
||||
params['host'] = model.host
|
||||
params['roomTabName'] = p.getHumanReadableName()
|
||||
params['chatNotificator'] = view.chatNotificator
|
||||
|
||||
mvcGroup.parentGroup.createMVCGroup("chat-room", groupId, params)
|
||||
}
|
||||
}
|
||||
|
||||
void markTrusted() {
|
||||
Persona p = view.getSelectedPersona()
|
||||
if (p == null)
|
||||
return
|
||||
String reason = JOptionPane.showInputDialog("Enter reason (optional)")
|
||||
model.core.eventBus.publish(new TrustEvent(persona : p, level : TrustLevel.TRUSTED, reason : reason))
|
||||
view.refreshMembersTable()
|
||||
}
|
||||
|
||||
void markDistrusted() {
|
||||
Persona p = view.getSelectedPersona()
|
||||
if (p == null)
|
||||
return
|
||||
String reason = JOptionPane.showInputDialog("Enter reason (optional)")
|
||||
model.core.eventBus.publish(new TrustEvent(persona : p, level : TrustLevel.DISTRUSTED, reason : reason))
|
||||
view.refreshMembersTable()
|
||||
}
|
||||
|
||||
void markNeutral() {
|
||||
Persona p = view.getSelectedPersona()
|
||||
if (p == null)
|
||||
return
|
||||
model.core.eventBus.publish(new TrustEvent(persona : p, level : TrustLevel.NEUTRAL))
|
||||
view.refreshMembersTable()
|
||||
}
|
||||
|
||||
void browse() {
|
||||
Persona p = view.getSelectedPersona()
|
||||
if (p == null)
|
||||
return
|
||||
String groupId = p.getHumanReadableName() + "-browse"
|
||||
def params = [:]
|
||||
params['host'] = p
|
||||
params['core'] = model.core
|
||||
mvcGroup.createMVCGroup("browse",groupId,params)
|
||||
}
|
||||
|
||||
void leaveRoom() {
|
||||
if (leftRoom)
|
||||
return
|
||||
leftRoom = true
|
||||
long now = System.currentTimeMillis()
|
||||
UUID uuid = UUID.randomUUID()
|
||||
byte [] sig = ChatConnection.sign(uuid, now, model.room, "/LEAVE", model.core.me, model.host, model.core.spk)
|
||||
def event = new ChatMessageEvent(uuid : uuid,
|
||||
payload : "/LEAVE",
|
||||
sender : model.core.me,
|
||||
host : model.host,
|
||||
room : model.room,
|
||||
chatTime : now,
|
||||
sig : sig)
|
||||
model.core.eventBus.publish(event)
|
||||
}
|
||||
|
||||
void handleChatMessage(ChatMessageEvent e) {
|
||||
ChatCommand command
|
||||
try {
|
||||
command = new ChatCommand(e.payload)
|
||||
} catch (Exception bad) {
|
||||
log.log(Level.WARNING,"bad chat command",bad)
|
||||
return
|
||||
}
|
||||
log.info("$model.room processing $command.action")
|
||||
switch(command.action) {
|
||||
case ChatAction.SAY : processSay(e, command.payload);break
|
||||
case ChatAction.JOIN : processJoin(e.timestamp, e.sender); break
|
||||
case ChatAction.JOINED : processJoined(command.payload); break
|
||||
case ChatAction.LEAVE : processLeave(e.timestamp, e.sender); break
|
||||
}
|
||||
}
|
||||
|
||||
private void processSay(ChatMessageEvent e, String text) {
|
||||
String toDisplay = DataHelper.formatTime(e.timestamp) + " <"+e.sender.getHumanReadableName()+"> " + text + "\n"
|
||||
runInsideUIAsync {
|
||||
view.roomTextArea.append(toDisplay)
|
||||
trimLines()
|
||||
if (!model.console)
|
||||
view.chatNotificator.onMessage(mvcGroup.mvcId)
|
||||
}
|
||||
}
|
||||
|
||||
private void processJoin(long timestamp, Persona p) {
|
||||
String toDisplay = DataHelper.formatTime(timestamp) + " " + p.getHumanReadableName() + " joined the room\n"
|
||||
runInsideUIAsync {
|
||||
model.members.add(p)
|
||||
view.roomTextArea.append(toDisplay)
|
||||
trimLines()
|
||||
view.membersTable?.model?.fireTableDataChanged()
|
||||
}
|
||||
}
|
||||
|
||||
private void processJoined(String list) {
|
||||
runInsideUIAsync {
|
||||
list.split(",").each {
|
||||
Persona p = new Persona(new ByteArrayInputStream(Base64.decode(it)))
|
||||
model.members.add(p)
|
||||
}
|
||||
view.membersTable?.model?.fireTableDataChanged()
|
||||
}
|
||||
}
|
||||
|
||||
private void processLeave(long timestamp, Persona p) {
|
||||
String toDisplay = DataHelper.formatTime(timestamp) + " " + p.getHumanReadableName() + " left the room\n"
|
||||
runInsideUIAsync {
|
||||
model.members.remove(p)
|
||||
view.roomTextArea.append(toDisplay)
|
||||
trimLines()
|
||||
view.membersTable?.model?.fireTableDataChanged()
|
||||
}
|
||||
}
|
||||
|
||||
void handleLeave(Persona p) {
|
||||
String toDisplay = DataHelper.formatTime(System.currentTimeMillis()) + " " + p.getHumanReadableName() + " disconnected\n"
|
||||
runInsideUIAsync {
|
||||
if (model.members.remove(p)) {
|
||||
view.roomTextArea.append(toDisplay)
|
||||
trimLines()
|
||||
view.membersTable?.model?.fireTableDataChanged()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void trimLines() {
|
||||
if (model.settings.maxChatLines < 0)
|
||||
return
|
||||
while(view.roomTextArea.getLineCount() > model.settings.maxChatLines) {
|
||||
int line0Start = view.roomTextArea.getLineStartOffset(0)
|
||||
int line0End = view.roomTextArea.getLineEndOffset(0)
|
||||
view.roomTextArea.replaceRange(null, line0Start, line0End)
|
||||
}
|
||||
}
|
||||
|
||||
void rejoinRoom() {
|
||||
if (model.room == "Console")
|
||||
return
|
||||
|
||||
model.members.clear()
|
||||
model.members.add(model.core.me)
|
||||
|
||||
UUID uuid = UUID.randomUUID()
|
||||
long now = System.currentTimeMillis()
|
||||
String join = "/JOIN $model.room"
|
||||
byte [] sig = ChatConnection.sign(uuid, now, ChatServer.CONSOLE, join, model.core.me, model.host, model.core.spk)
|
||||
def event = new ChatMessageEvent(
|
||||
uuid : uuid,
|
||||
payload : join,
|
||||
sender : model.core.me,
|
||||
host : model.host,
|
||||
room : ChatServer.CONSOLE,
|
||||
chatTime : now,
|
||||
sig : sig
|
||||
)
|
||||
model.core.eventBus.publish(event)
|
||||
}
|
||||
}
|
@@ -0,0 +1,28 @@
|
||||
package com.muwire.gui
|
||||
|
||||
import griffon.core.artifact.GriffonController
|
||||
import griffon.core.controller.ControllerAction
|
||||
import griffon.inject.MVCMember
|
||||
import griffon.metadata.ArtifactProviderFor
|
||||
import javax.annotation.Nonnull
|
||||
|
||||
import com.muwire.core.chat.UIDisconnectChatEvent
|
||||
|
||||
@ArtifactProviderFor(GriffonController)
|
||||
class ChatServerController {
|
||||
@MVCMember @Nonnull
|
||||
ChatServerModel model
|
||||
|
||||
@ControllerAction
|
||||
void disconnect() {
|
||||
switch(model.buttonText) {
|
||||
case "Disconnect" :
|
||||
model.buttonText = "Connect"
|
||||
model.core.eventBus.publish(new UIDisconnectChatEvent(host : model.host))
|
||||
break
|
||||
case "Connect" :
|
||||
model.connect()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
@@ -5,6 +5,7 @@ import griffon.core.controller.ControllerAction
|
||||
import griffon.inject.MVCMember
|
||||
import griffon.metadata.ArtifactProviderFor
|
||||
import javax.annotation.Nonnull
|
||||
import javax.swing.JOptionPane
|
||||
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.EventBus
|
||||
@@ -83,8 +84,9 @@ class ContentPanelController {
|
||||
int selectedHit = view.getSelectedHit()
|
||||
if (selectedHit < 0)
|
||||
return
|
||||
String reason = JOptionPane.showInputDialog("Enter reason (optional)")
|
||||
Match m = model.hits[selectedHit]
|
||||
core.eventBus.publish(new TrustEvent(persona : m.persona, level : TrustLevel.TRUSTED))
|
||||
core.eventBus.publish(new TrustEvent(persona : m.persona, level : TrustLevel.TRUSTED, reason : reason))
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
@@ -92,8 +94,9 @@ class ContentPanelController {
|
||||
int selectedHit = view.getSelectedHit()
|
||||
if (selectedHit < 0)
|
||||
return
|
||||
String reason = JOptionPane.showInputDialog("Enter reason (optional)")
|
||||
Match m = model.hits[selectedHit]
|
||||
core.eventBus.publish(new TrustEvent(persona : m.persona, level : TrustLevel.DISTRUSTED))
|
||||
core.eventBus.publish(new TrustEvent(persona : m.persona, level : TrustLevel.DISTRUSTED, reason : reason))
|
||||
}
|
||||
|
||||
void saveMuWireSettings() {
|
||||
|
@@ -0,0 +1,13 @@
|
||||
package com.muwire.gui
|
||||
|
||||
import griffon.core.artifact.GriffonController
|
||||
import griffon.core.controller.ControllerAction
|
||||
import griffon.inject.MVCMember
|
||||
import griffon.metadata.ArtifactProviderFor
|
||||
import javax.annotation.Nonnull
|
||||
|
||||
@ArtifactProviderFor(GriffonController)
|
||||
class DownloadPreviewController {
|
||||
@MVCMember @Nonnull
|
||||
DownloadPreviewModel model
|
||||
}
|
@@ -7,10 +7,13 @@ import griffon.core.mvc.MVCGroup
|
||||
import griffon.core.mvc.MVCGroupConfiguration
|
||||
import griffon.inject.MVCMember
|
||||
import griffon.metadata.ArtifactProviderFor
|
||||
import groovy.json.StringEscapeUtils
|
||||
import net.i2p.crypto.DSAEngine
|
||||
import net.i2p.data.Base64
|
||||
import net.i2p.data.Signature
|
||||
import net.i2p.data.SigningPrivateKey
|
||||
|
||||
import java.awt.Desktop
|
||||
import java.awt.event.ActionEvent
|
||||
import java.nio.charset.StandardCharsets
|
||||
|
||||
@@ -42,6 +45,7 @@ import com.muwire.core.trust.TrustLevel
|
||||
import com.muwire.core.trust.TrustSubscriptionEvent
|
||||
import com.muwire.core.upload.HashListUploader
|
||||
import com.muwire.core.upload.Uploader
|
||||
import com.muwire.core.util.DataUtil
|
||||
|
||||
@ArtifactProviderFor(GriffonController)
|
||||
class MainFrameController {
|
||||
@@ -90,6 +94,7 @@ class MainFrameController {
|
||||
params["search-terms"] = search
|
||||
params["uuid"] = uuid.toString()
|
||||
params["core"] = core
|
||||
params["settings"] = view.settings
|
||||
def group = mvcGroup.createMVCGroup("SearchTab", uuid.toString(), params)
|
||||
model.results[uuid.toString()] = group
|
||||
|
||||
@@ -119,9 +124,10 @@ class MainFrameController {
|
||||
|
||||
Signature sig = DSAEngine.getInstance().sign(payload, core.spk)
|
||||
|
||||
long timestamp = System.currentTimeMillis()
|
||||
core.eventBus.publish(new QueryEvent(searchEvent : searchEvent, firstHop : firstHop,
|
||||
replyTo: core.me.destination, receivedOn: core.me.destination,
|
||||
originator : core.me, sig : sig.data))
|
||||
originator : core.me, sig : sig.data, queryTime : timestamp, sig2 : DataUtil.signUUID(uuid, timestamp, core.spk)))
|
||||
|
||||
}
|
||||
|
||||
@@ -138,14 +144,16 @@ class MainFrameController {
|
||||
|
||||
byte [] infoHashBytes = Base64.decode(infoHash)
|
||||
Signature sig = DSAEngine.getInstance().sign(infoHashBytes, core.spk)
|
||||
long timestamp = System.currentTimeMillis()
|
||||
byte [] sig2 = DataUtil.signUUID(uuid, timestamp, core.spk)
|
||||
|
||||
def searchEvent = new SearchEvent(searchHash : Base64.decode(infoHash), uuid:uuid,
|
||||
oobInfohash: true, persona : core.me)
|
||||
core.eventBus.publish(new QueryEvent(searchEvent : searchEvent, firstHop : true,
|
||||
replyTo: core.me.destination, receivedOn: core.me.destination,
|
||||
originator : core.me, sig : sig.data))
|
||||
originator : core.me, sig : sig.data, queryTime : timestamp, sig2 : sig2))
|
||||
}
|
||||
|
||||
|
||||
private int selectedDownload() {
|
||||
def downloadsTable = builder.getVariable("downloads-table")
|
||||
def selected = downloadsTable.getSelectedRow()
|
||||
@@ -160,8 +168,9 @@ class MainFrameController {
|
||||
int selected = builder.getVariable("searches-table").getSelectedRow()
|
||||
if (selected < 0)
|
||||
return
|
||||
String reason = JOptionPane.showInputDialog("Enter reason (optional)")
|
||||
Persona p = model.searches[selected].originator
|
||||
core.eventBus.publish( new TrustEvent(persona : p, level : TrustLevel.TRUSTED) )
|
||||
core.eventBus.publish( new TrustEvent(persona : p, level : TrustLevel.TRUSTED, reason : reason) )
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
@@ -169,8 +178,9 @@ class MainFrameController {
|
||||
int selected = builder.getVariable("searches-table").getSelectedRow()
|
||||
if (selected < 0)
|
||||
return
|
||||
String reason = JOptionPane.showInputDialog("Enter reason (optional)")
|
||||
Persona p = model.searches[selected].originator
|
||||
core.eventBus.publish( new TrustEvent(persona : p, level : TrustLevel.DISTRUSTED) )
|
||||
core.eventBus.publish( new TrustEvent(persona : p, level : TrustLevel.DISTRUSTED, reason : reason) )
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
@@ -194,6 +204,14 @@ class MainFrameController {
|
||||
downloader.pause()
|
||||
core.eventBus.publish(new UIDownloadPausedEvent())
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void preview() {
|
||||
def downloader = model.downloads[selectedDownload()].downloader
|
||||
def params = [:]
|
||||
params['downloader'] = downloader
|
||||
mvcGroup.createMVCGroup("download-preview", params)
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void clear() {
|
||||
@@ -216,8 +234,11 @@ class MainFrameController {
|
||||
int row = view.getSelectedTrustTablesRow(tableName)
|
||||
if (row < 0)
|
||||
return
|
||||
String reason = null
|
||||
if (level != TrustLevel.NEUTRAL)
|
||||
reason = JOptionPane.showInputDialog("Enter reason (optional)")
|
||||
builder.getVariable(tableName).model.fireTableDataChanged()
|
||||
core.eventBus.publish(new TrustEvent(persona : list[row], level : level))
|
||||
core.eventBus.publish(new TrustEvent(persona : list[row].persona, level : level, reason : reason))
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
@@ -255,7 +276,7 @@ class MainFrameController {
|
||||
int row = view.getSelectedTrustTablesRow("trusted-table")
|
||||
if (row < 0)
|
||||
return
|
||||
Persona p = model.trusted[row]
|
||||
Persona p = model.trusted[row].persona
|
||||
core.muOptions.trustSubscriptions.add(p)
|
||||
saveMuWireSettings()
|
||||
core.eventBus.publish(new TrustSubscriptionEvent(persona : p, subscribe : true))
|
||||
@@ -304,6 +325,31 @@ class MainFrameController {
|
||||
return null
|
||||
model.subscriptions[row]
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void browseFromTrusted() {
|
||||
int row = view.getSelectedTrustTablesRow("trusted-table")
|
||||
if (row < 0)
|
||||
return
|
||||
Persona p = model.trusted[row].persona
|
||||
|
||||
String groupId = p.getHumanReadableName() + "-browse"
|
||||
def params = [:]
|
||||
params['host'] = p
|
||||
params['core'] = model.core
|
||||
mvcGroup.createMVCGroup("browse",groupId,params)
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void chatFromTrusted() {
|
||||
int row = view.getSelectedTrustTablesRow("trusted-table")
|
||||
if (row < 0)
|
||||
return
|
||||
Persona p = model.trusted[row].persona
|
||||
|
||||
startChat(p)
|
||||
view.showChatWindow.call()
|
||||
}
|
||||
|
||||
void unshareSelectedFile() {
|
||||
def sf = view.selectedSharedFiles()
|
||||
@@ -380,7 +426,7 @@ class MainFrameController {
|
||||
@ControllerAction
|
||||
void showFileDetails() {
|
||||
def selected = view.selectedSharedFiles()
|
||||
if (selected.size() != 1) {
|
||||
if (selected == null || selected.size() != 1) {
|
||||
JOptionPane.showMessageDialog(null, "Please select only one file to view it's details")
|
||||
return
|
||||
}
|
||||
@@ -389,6 +435,66 @@ class MainFrameController {
|
||||
params['core'] = core
|
||||
mvcGroup.createMVCGroup("shared-file", params)
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void openContainingFolder() {
|
||||
def selected = view.selectedSharedFiles()
|
||||
if (selected == null || selected.size() != 1) {
|
||||
JOptionPane.showMessageDialog(null, "Please select only one file to open it's containing folder")
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
Desktop.getDesktop().open(selected[0].file.getParentFile())
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void startChatServer() {
|
||||
model.core.chatServer.start()
|
||||
model.chatServerRunning = true
|
||||
|
||||
if (!mvcGroup.getChildrenGroups().containsKey("local-chat-server")) {
|
||||
def params = [:]
|
||||
params['core'] = model.core
|
||||
params['host'] = model.core.me
|
||||
params['chatNotificator'] = view.chatNotificator
|
||||
mvcGroup.createMVCGroup("chat-server","local-chat-server", params)
|
||||
}
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void stopChatServer() {
|
||||
model.core.chatServer.stop()
|
||||
model.chatServerRunning = false
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void connectChatServer() {
|
||||
String address = JOptionPane.showInputDialog("Copy/paste the address of the server here")
|
||||
if (address == null)
|
||||
return
|
||||
Persona p
|
||||
try {
|
||||
p = new Persona(new ByteArrayInputStream(Base64.decode(address)))
|
||||
} catch (Exception bad) {
|
||||
JOptionPane.showMessageDialog(null, "Invalid server address", "Invalid server address", JOptionPane.ERROR_MESSAGE)
|
||||
return
|
||||
}
|
||||
|
||||
startChat(p)
|
||||
}
|
||||
|
||||
void startChat(Persona p) {
|
||||
if (!mvcGroup.getChildrenGroups().containsKey(p.getHumanReadableName())) {
|
||||
def params = [:]
|
||||
params['core'] = model.core
|
||||
params['host'] = p
|
||||
params['chatNotificator'] = view.chatNotificator
|
||||
mvcGroup.createMVCGroup("chat-server", p.getHumanReadableName(), params)
|
||||
} else
|
||||
mvcGroup.getChildrenGroups().get(p.getHumanReadableName()).model.connect()
|
||||
}
|
||||
|
||||
void saveMuWireSettings() {
|
||||
core.saveMuSettings()
|
||||
|
@@ -139,6 +139,22 @@ class OptionsController {
|
||||
String trustListInterval = view.trustListIntervalField.text
|
||||
model.trustListInterval = trustListInterval
|
||||
settings.trustListInterval = Integer.parseInt(trustListInterval)
|
||||
|
||||
boolean startChatServer = view.startChatServerCheckbox.model.isSelected()
|
||||
model.startChatServer = startChatServer
|
||||
settings.startChatServer = startChatServer
|
||||
|
||||
String maxChatConnections = view.maxChatConnectionsField.text
|
||||
model.maxChatConnections = Integer.parseInt(maxChatConnections)
|
||||
settings.maxChatConnections = Integer.parseInt(maxChatConnections)
|
||||
|
||||
boolean advertiseChat = view.advertiseChatCheckbox.model.isSelected()
|
||||
model.advertiseChat = advertiseChat
|
||||
settings.advertiseChat = advertiseChat
|
||||
|
||||
int maxChatLines = Integer.parseInt(view.maxChatLinesField.text)
|
||||
model.maxChatLines = maxChatLines
|
||||
uiSettings.maxChatLines = maxChatLines
|
||||
|
||||
core.saveMuSettings()
|
||||
|
||||
@@ -155,6 +171,8 @@ class OptionsController {
|
||||
uiSettings.autoFontSize = model.automaticFontSize
|
||||
uiSettings.fontSize = Integer.parseInt(view.fontSizeField.text)
|
||||
|
||||
uiSettings.groupByFile = model.groupByFile
|
||||
|
||||
boolean clearCancelledDownloads = view.clearCancelledDownloadsCheckbox.model.isSelected()
|
||||
model.clearCancelledDownloads = clearCancelledDownloads
|
||||
uiSettings.clearCancelledDownloads = clearCancelledDownloads
|
||||
@@ -242,6 +260,16 @@ class OptionsController {
|
||||
model.closeDecisionMade = true
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void groupByFile() {
|
||||
model.groupByFile = true
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void groupBySender() {
|
||||
model.groupByFile = false
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void clearHistory() {
|
||||
uiSettings.searchHistory.clear()
|
||||
|
@@ -7,6 +7,7 @@ import griffon.metadata.ArtifactProviderFor
|
||||
import net.i2p.data.Base64
|
||||
|
||||
import javax.annotation.Nonnull
|
||||
import javax.swing.JOptionPane
|
||||
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.Persona
|
||||
@@ -26,117 +27,120 @@ class SearchTabController {
|
||||
Core core
|
||||
|
||||
private def selectedResults() {
|
||||
int[] rows = view.resultsTable.getSelectedRows()
|
||||
if (rows.length == 0)
|
||||
return null
|
||||
def sortEvt = view.lastSortEvent
|
||||
if (sortEvt != null) {
|
||||
for (int i = 0; i < rows.length; i++) {
|
||||
rows[i] = view.resultsTable.rowSorter.convertRowIndexToModel(rows[i])
|
||||
if (model.groupedByFile) {
|
||||
return [view.getSelectedResult()]
|
||||
} else {
|
||||
int[] rows = view.resultsTable.getSelectedRows()
|
||||
if (rows.length == 0)
|
||||
return null
|
||||
def sortEvt = view.lastSortEvent
|
||||
if (sortEvt != null) {
|
||||
for (int i = 0; i < rows.length; i++) {
|
||||
rows[i] = view.resultsTable.rowSorter.convertRowIndexToModel(rows[i])
|
||||
}
|
||||
}
|
||||
List<UIResultEvent> results = new ArrayList<>()
|
||||
rows.each { results.add(model.results[it]) }
|
||||
return results
|
||||
}
|
||||
List<UIResultEvent> results = new ArrayList<>()
|
||||
rows.each { results.add(model.results[it]) }
|
||||
results
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void download() {
|
||||
def results = selectedResults()
|
||||
if (results == null)
|
||||
return
|
||||
|
||||
results.removeAll {
|
||||
!mvcGroup.parentGroup.model.canDownload(it.infohash)
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void download() {
|
||||
def results = selectedResults()
|
||||
if (results == null)
|
||||
return
|
||||
results.each { result ->
|
||||
def file = new File(application.context.get("muwire-settings").downloadLocation, result.name)
|
||||
|
||||
results.removeAll {
|
||||
!mvcGroup.parentGroup.model.canDownload(it.infohash)
|
||||
}
|
||||
def resultsBucket = model.hashBucket[result.infohash]
|
||||
def sources = model.sourcesBucket[result.infohash]
|
||||
|
||||
results.each { result ->
|
||||
def file = new File(application.context.get("muwire-settings").downloadLocation, result.name)
|
||||
|
||||
def resultsBucket = model.hashBucket[result.infohash]
|
||||
def sources = model.sourcesBucket[result.infohash]
|
||||
|
||||
core.eventBus.publish(new UIDownloadEvent(result : resultsBucket, sources: sources,
|
||||
target : file, sequential : view.sequentialDownloadCheckbox.model.isSelected()))
|
||||
}
|
||||
mvcGroup.parentGroup.view.showDownloadsWindow.call()
|
||||
core.eventBus.publish(new UIDownloadEvent(result : resultsBucket, sources: sources,
|
||||
target : file, sequential : view.sequentialDownload()))
|
||||
}
|
||||
mvcGroup.parentGroup.view.showDownloadsWindow.call()
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void trust() {
|
||||
int row = view.selectedSenderRow()
|
||||
if (row < 0)
|
||||
return
|
||||
def sender = model.senders[row]
|
||||
core.eventBus.publish( new TrustEvent(persona : sender, level : TrustLevel.TRUSTED))
|
||||
}
|
||||
@ControllerAction
|
||||
void trust() {
|
||||
def sender = view.selectedSender()
|
||||
if (sender == null)
|
||||
return
|
||||
String reason = JOptionPane.showInputDialog("Enter reason (optional)")
|
||||
core.eventBus.publish( new TrustEvent(persona : sender, level : TrustLevel.TRUSTED, reason : reason))
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void distrust() {
|
||||
int row = view.selectedSenderRow()
|
||||
if (row < 0)
|
||||
return
|
||||
def sender = model.senders[row]
|
||||
core.eventBus.publish( new TrustEvent(persona : sender, level : TrustLevel.DISTRUSTED))
|
||||
}
|
||||
@ControllerAction
|
||||
void distrust() {
|
||||
def sender = view.selectedSender()
|
||||
if (sender == null)
|
||||
return
|
||||
String reason = JOptionPane.showInputDialog("Enter reason (optional)")
|
||||
core.eventBus.publish( new TrustEvent(persona : sender, level : TrustLevel.DISTRUSTED, reason : reason))
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void neutral() {
|
||||
int row = view.selectedSenderRow()
|
||||
if (row < 0)
|
||||
return
|
||||
def sender = model.senders[row]
|
||||
core.eventBus.publish( new TrustEvent(persona : sender, level : TrustLevel.NEUTRAL))
|
||||
}
|
||||
@ControllerAction
|
||||
void neutral() {
|
||||
def sender = view.selectedSender()
|
||||
if (sender == null)
|
||||
return
|
||||
core.eventBus.publish( new TrustEvent(persona : sender, level : TrustLevel.NEUTRAL))
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void browse() {
|
||||
def sender = view.selectedSender()
|
||||
if (sender == null)
|
||||
return
|
||||
|
||||
String groupId = sender.getHumanReadableName() + "-browse"
|
||||
Map<String,Object> params = new HashMap<>()
|
||||
params['host'] = sender
|
||||
params['core'] = core
|
||||
|
||||
mvcGroup.createMVCGroup("browse", groupId, params)
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void chat() {
|
||||
def sender = view.selectedSender()
|
||||
if (sender == null)
|
||||
return
|
||||
|
||||
@ControllerAction
|
||||
void browse() {
|
||||
int selectedSender = view.selectedSenderRow()
|
||||
if (selectedSender < 0)
|
||||
return
|
||||
Persona sender = model.senders[selectedSender]
|
||||
|
||||
String groupId = sender.getHumanReadableName()
|
||||
Map<String,Object> params = new HashMap<>()
|
||||
params['host'] = sender
|
||||
params['core'] = core
|
||||
|
||||
mvcGroup.createMVCGroup("browse", groupId, params)
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void showComment() {
|
||||
int[] selectedRows = view.resultsTable.getSelectedRows()
|
||||
if (selectedRows.length != 1)
|
||||
return
|
||||
if (view.lastSortEvent != null)
|
||||
selectedRows[0] = view.resultsTable.rowSorter.convertRowIndexToModel(selectedRows[0])
|
||||
UIResultEvent event = model.results[selectedRows[0]]
|
||||
if (event.comment == null)
|
||||
return
|
||||
|
||||
String groupId = Base64.encode(event.infohash.getRoot())
|
||||
Map<String,Object> params = new HashMap<>()
|
||||
params['text'] = event.comment
|
||||
params['name'] = event.name
|
||||
|
||||
mvcGroup.createMVCGroup("show-comment", groupId, params)
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void viewCertificates() {
|
||||
int[] selectedRows = view.resultsTable.getSelectedRows()
|
||||
if (selectedRows.length != 1)
|
||||
return
|
||||
if (view.lastSortEvent != null)
|
||||
selectedRows[0] = view.resultsTable.rowSorter.convertRowIndexToModel(selectedRows[0])
|
||||
UIResultEvent event = model.results[selectedRows[0]]
|
||||
if (event.certificates <= 0)
|
||||
return
|
||||
|
||||
def params = [:]
|
||||
params['result'] = event
|
||||
params['core'] = core
|
||||
mvcGroup.createMVCGroup("fetch-certificates", params)
|
||||
}
|
||||
}
|
||||
def parent = mvcGroup.parentGroup
|
||||
parent.controller.startChat(sender)
|
||||
parent.view.showChatWindow.call()
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void showComment() {
|
||||
UIResultEvent event = view.getSelectedResult()
|
||||
if (event == null || event.comment == null)
|
||||
return
|
||||
|
||||
String groupId = Base64.encode(event.infohash.getRoot())
|
||||
Map<String,Object> params = new HashMap<>()
|
||||
params['text'] = event.comment
|
||||
params['name'] = event.name
|
||||
|
||||
mvcGroup.createMVCGroup("show-comment", groupId, params)
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void viewCertificates() {
|
||||
UIResultEvent event = view.getSelectedResult()
|
||||
if (event == null || event.certificates <= 0)
|
||||
return
|
||||
|
||||
def params = [:]
|
||||
params['result'] = event
|
||||
params['core'] = core
|
||||
mvcGroup.createMVCGroup("fetch-certificates", params)
|
||||
}
|
||||
}
|
@@ -6,6 +6,23 @@ import griffon.inject.MVCMember
|
||||
import griffon.metadata.ArtifactProviderFor
|
||||
import javax.annotation.Nonnull
|
||||
|
||||
import com.muwire.core.filecert.Certificate
|
||||
|
||||
@ArtifactProviderFor(GriffonController)
|
||||
class SharedFileController {
|
||||
@MVCMember @Nonnull
|
||||
SharedFileView view
|
||||
@MVCMember @Nonnull
|
||||
SharedFileModel model
|
||||
|
||||
@ControllerAction
|
||||
void showComment() {
|
||||
Certificate cert = view.getSelectedCertificate()
|
||||
if (cert == null || cert.comment == null)
|
||||
return
|
||||
|
||||
def params = [:]
|
||||
params['text'] = cert.comment.name
|
||||
mvcGroup.createMVCGroup('show-comment',params)
|
||||
}
|
||||
}
|
@@ -5,6 +5,7 @@ import griffon.core.controller.ControllerAction
|
||||
import griffon.inject.MVCMember
|
||||
import griffon.metadata.ArtifactProviderFor
|
||||
import javax.annotation.Nonnull
|
||||
import javax.swing.JOptionPane
|
||||
|
||||
import com.muwire.core.EventBus
|
||||
import com.muwire.core.Persona
|
||||
@@ -25,8 +26,9 @@ class TrustListController {
|
||||
int selectedRow = view.getSelectedRow("trusted-table")
|
||||
if (selectedRow < 0)
|
||||
return
|
||||
Persona p = model.trusted[selectedRow]
|
||||
eventBus.publish(new TrustEvent(persona : p, level : TrustLevel.TRUSTED))
|
||||
String reason = JOptionPane.showInputDialog("Enter reason (optional)")
|
||||
Persona p = model.trusted[selectedRow].persona
|
||||
eventBus.publish(new TrustEvent(persona : p, level : TrustLevel.TRUSTED, reason : reason))
|
||||
view.fireUpdate("trusted-table")
|
||||
}
|
||||
|
||||
@@ -35,8 +37,9 @@ class TrustListController {
|
||||
int selectedRow = view.getSelectedRow("distrusted-table")
|
||||
if (selectedRow < 0)
|
||||
return
|
||||
Persona p = model.distrusted[selectedRow]
|
||||
eventBus.publish(new TrustEvent(persona : p, level : TrustLevel.TRUSTED))
|
||||
String reason = JOptionPane.showInputDialog("Enter reason (optional)")
|
||||
Persona p = model.distrusted[selectedRow].persona
|
||||
eventBus.publish(new TrustEvent(persona : p, level : TrustLevel.TRUSTED, reason : reason))
|
||||
view.fireUpdate("distrusted-table")
|
||||
}
|
||||
|
||||
@@ -45,8 +48,9 @@ class TrustListController {
|
||||
int selectedRow = view.getSelectedRow("trusted-table")
|
||||
if (selectedRow < 0)
|
||||
return
|
||||
Persona p = model.trusted[selectedRow]
|
||||
eventBus.publish(new TrustEvent(persona : p, level : TrustLevel.DISTRUSTED))
|
||||
String reason = JOptionPane.showInputDialog("Enter reason (optional)")
|
||||
Persona p = model.trusted[selectedRow].persona
|
||||
eventBus.publish(new TrustEvent(persona : p, level : TrustLevel.DISTRUSTED, reason : reason))
|
||||
view.fireUpdate("trusted-table")
|
||||
}
|
||||
|
||||
@@ -55,8 +59,9 @@ class TrustListController {
|
||||
int selectedRow = view.getSelectedRow("distrusted-table")
|
||||
if (selectedRow < 0)
|
||||
return
|
||||
Persona p = model.distrusted[selectedRow]
|
||||
eventBus.publish(new TrustEvent(persona : p, level : TrustLevel.DISTRUSTED))
|
||||
String reason = JOptionPane.showInputDialog("Enter reason (optional)")
|
||||
Persona p = model.distrusted[selectedRow].persona
|
||||
eventBus.publish(new TrustEvent(persona : p, level : TrustLevel.DISTRUSTED, reason : reason))
|
||||
view.fireUpdate("distrusted-table")
|
||||
}
|
||||
}
|
28
gui/griffon-app/models/com/muwire/gui/ChatRoomModel.groovy
Normal file
28
gui/griffon-app/models/com/muwire/gui/ChatRoomModel.groovy
Normal file
@@ -0,0 +1,28 @@
|
||||
package com.muwire.gui
|
||||
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.Persona
|
||||
|
||||
import griffon.core.artifact.GriffonModel
|
||||
import griffon.transform.Observable
|
||||
import griffon.metadata.ArtifactProviderFor
|
||||
|
||||
@ArtifactProviderFor(GriffonModel)
|
||||
class ChatRoomModel {
|
||||
Core core
|
||||
Persona host
|
||||
String tabName
|
||||
String room
|
||||
boolean console
|
||||
boolean privateChat
|
||||
String roomTabName
|
||||
|
||||
def members = []
|
||||
|
||||
UISettings settings
|
||||
|
||||
void mvcGroupInit(Map<String,String> args) {
|
||||
members.add(core.me)
|
||||
settings = application.context.get("ui-settings")
|
||||
}
|
||||
}
|
157
gui/griffon-app/models/com/muwire/gui/ChatServerModel.groovy
Normal file
157
gui/griffon-app/models/com/muwire/gui/ChatServerModel.groovy
Normal file
@@ -0,0 +1,157 @@
|
||||
package com.muwire.gui
|
||||
|
||||
import java.util.logging.Level
|
||||
|
||||
import javax.annotation.Nonnull
|
||||
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.chat.ChatCommand
|
||||
import com.muwire.core.chat.ChatAction
|
||||
import com.muwire.core.chat.ChatConnectionAttemptStatus
|
||||
import com.muwire.core.chat.ChatConnectionEvent
|
||||
import com.muwire.core.chat.ChatLink
|
||||
import com.muwire.core.chat.ChatMessageEvent
|
||||
import com.muwire.core.chat.UIConnectChatEvent
|
||||
|
||||
import griffon.core.artifact.GriffonModel
|
||||
import griffon.inject.MVCMember
|
||||
import griffon.transform.Observable
|
||||
import groovy.util.logging.Log
|
||||
import griffon.metadata.ArtifactProviderFor
|
||||
|
||||
@Log
|
||||
@ArtifactProviderFor(GriffonModel)
|
||||
class ChatServerModel {
|
||||
@MVCMember @Nonnull
|
||||
ChatServerView view
|
||||
|
||||
Persona host
|
||||
Core core
|
||||
|
||||
@Observable boolean disconnectActionEnabled
|
||||
@Observable String buttonText = "Disconnect"
|
||||
@Observable ChatConnectionAttemptStatus status
|
||||
@Observable boolean sayActionEnabled
|
||||
|
||||
volatile ChatLink link
|
||||
volatile Thread poller
|
||||
volatile boolean running
|
||||
|
||||
void mvcGroupInit(Map<String, String> params) {
|
||||
disconnectActionEnabled = host != core.me // can't disconnect from myself
|
||||
core.eventBus.register(ChatConnectionEvent.class, this)
|
||||
|
||||
connect()
|
||||
}
|
||||
|
||||
void connect() {
|
||||
runInsideUIAsync {
|
||||
buttonText = "Disconnect"
|
||||
}
|
||||
core.eventBus.publish(new UIConnectChatEvent(host : host))
|
||||
}
|
||||
|
||||
void mvcGroupDestroy() {
|
||||
stopPoller()
|
||||
core.eventBus.unregister(ChatConnectionEvent.class, this)
|
||||
}
|
||||
|
||||
private void startPoller() {
|
||||
if (running)
|
||||
return
|
||||
running = true
|
||||
poller = new Thread({eventLoop()} as Runnable)
|
||||
poller.setDaemon(true)
|
||||
poller.start()
|
||||
}
|
||||
|
||||
private void stopPoller() {
|
||||
running = false
|
||||
poller?.interrupt()
|
||||
link = null
|
||||
}
|
||||
|
||||
void onChatConnectionEvent(ChatConnectionEvent e) {
|
||||
if (e.persona != host)
|
||||
return
|
||||
|
||||
runInsideUIAsync {
|
||||
status = e.status
|
||||
sayActionEnabled = status == ChatConnectionAttemptStatus.SUCCESSFUL
|
||||
}
|
||||
|
||||
if (e.status == ChatConnectionAttemptStatus.SUCCESSFUL) {
|
||||
ChatLink link = e.connection
|
||||
if (link == null)
|
||||
return
|
||||
this.link = e.connection
|
||||
|
||||
startPoller()
|
||||
|
||||
mvcGroup.childrenGroups.each {k,v ->
|
||||
v.controller.rejoinRoom()
|
||||
}
|
||||
} else {
|
||||
stopPoller()
|
||||
}
|
||||
}
|
||||
|
||||
private void eventLoop() {
|
||||
Thread.sleep(1000)
|
||||
while(running) {
|
||||
ChatLink link = this.link
|
||||
if (link == null || !link.isUp()) {
|
||||
Thread.sleep(100)
|
||||
continue
|
||||
}
|
||||
|
||||
Object event = link.nextEvent()
|
||||
if (event instanceof ChatMessageEvent)
|
||||
handleChatMessage(event)
|
||||
else if (event instanceof Persona)
|
||||
handleLeave(event)
|
||||
else
|
||||
throw new IllegalArgumentException("event type $event")
|
||||
}
|
||||
}
|
||||
|
||||
private void handleChatMessage(ChatMessageEvent e) {
|
||||
ChatCommand chatCommand
|
||||
try {
|
||||
chatCommand = new ChatCommand(e.payload)
|
||||
} catch (Exception badCommand) {
|
||||
log.log(Level.WARNING,"bad chat command",badCommand)
|
||||
return
|
||||
}
|
||||
String room = e.room
|
||||
if (chatCommand.action == ChatAction.JOIN) {
|
||||
room = chatCommand.payload
|
||||
}
|
||||
if (chatCommand.action == ChatAction.SAY &&
|
||||
room == core.me.toBase64()) {
|
||||
String groupId = host.getHumanReadableName()+"-"+e.sender.getHumanReadableName() + "-private-chat"
|
||||
if (!mvcGroup.childrenGroups.containsKey(groupId)) {
|
||||
def params = [:]
|
||||
params['core'] = core
|
||||
params['tabName'] = host.getHumanReadableName() + "-chat-rooms"
|
||||
params['room'] = e.sender.toBase64()
|
||||
params['privateChat'] = true
|
||||
params['host'] = host
|
||||
params['roomTabName'] = e.sender.getHumanReadableName()
|
||||
params['chatNotificator'] = view.chatNotificator
|
||||
|
||||
mvcGroup.createMVCGroup("chat-room",groupId, params)
|
||||
}
|
||||
room = groupId
|
||||
} else
|
||||
room = host.getHumanReadableName()+"-"+room
|
||||
mvcGroup.childrenGroups[room]?.controller?.handleChatMessage(e)
|
||||
}
|
||||
|
||||
private void handleLeave(Persona p) {
|
||||
mvcGroup.childrenGroups.each { k, v ->
|
||||
v.controller.handleLeave(p)
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,12 @@
|
||||
package com.muwire.gui
|
||||
|
||||
import com.muwire.core.download.Downloader
|
||||
|
||||
import griffon.core.artifact.GriffonModel
|
||||
import griffon.transform.Observable
|
||||
import griffon.metadata.ArtifactProviderFor
|
||||
|
||||
@ArtifactProviderFor(GriffonModel)
|
||||
class DownloadPreviewModel {
|
||||
Downloader downloader
|
||||
}
|
@@ -100,11 +100,14 @@ class MainFrameModel {
|
||||
@Observable boolean retryButtonEnabled
|
||||
@Observable boolean pauseButtonEnabled
|
||||
@Observable boolean clearButtonEnabled
|
||||
@Observable boolean previewButtonEnabled
|
||||
@Observable String resumeButtonText
|
||||
@Observable boolean addCommentButtonEnabled
|
||||
@Observable boolean subscribeButtonEnabled
|
||||
@Observable boolean markNeutralFromTrustedButtonEnabled
|
||||
@Observable boolean markDistrustedButtonEnabled
|
||||
@Observable boolean browseFromTrustedButtonEnabled
|
||||
@Observable boolean chatFromTrustedButtonEnabled
|
||||
@Observable boolean markNeutralFromDistrustedButtonEnabled
|
||||
@Observable boolean markTrustedButtonEnabled
|
||||
@Observable boolean reviewButtonEnabled
|
||||
@@ -116,6 +119,9 @@ class MainFrameModel {
|
||||
@Observable boolean uploadsPaneButtonEnabled
|
||||
@Observable boolean monitorPaneButtonEnabled
|
||||
@Observable boolean trustPaneButtonEnabled
|
||||
@Observable boolean chatPaneButtonEnabled
|
||||
|
||||
@Observable boolean chatServerRunning
|
||||
|
||||
@Observable Downloader downloader
|
||||
|
||||
@@ -217,6 +223,8 @@ class MainFrameModel {
|
||||
core.eventBus.publish(new ContentControlEvent(term : it, regex: true, add: true))
|
||||
}
|
||||
|
||||
chatServerRunning = core.chatServer.running.get()
|
||||
|
||||
timer.schedule({
|
||||
if (core.shutdown.get())
|
||||
return
|
||||
@@ -251,6 +259,10 @@ class MainFrameModel {
|
||||
uploadsPaneButtonEnabled = true
|
||||
monitorPaneButtonEnabled = true
|
||||
trustPaneButtonEnabled = true
|
||||
chatPaneButtonEnabled = true
|
||||
|
||||
if (core.muOptions.startChatServer)
|
||||
controller.startChatServer()
|
||||
}
|
||||
})
|
||||
|
||||
|
@@ -42,6 +42,7 @@ class OptionsModel {
|
||||
@Observable boolean excludeLocalResult
|
||||
@Observable boolean showSearchHashes
|
||||
@Observable boolean clearUploads
|
||||
@Observable boolean groupByFile
|
||||
@Observable boolean exitOnClose
|
||||
@Observable boolean closeDecisionMade
|
||||
|
||||
@@ -55,6 +56,11 @@ class OptionsModel {
|
||||
@Observable boolean trustLists
|
||||
@Observable String trustListInterval
|
||||
|
||||
// chat options
|
||||
@Observable boolean startChatServer
|
||||
@Observable int maxChatConnections
|
||||
@Observable boolean advertiseChat
|
||||
@Observable int maxChatLines
|
||||
|
||||
void mvcGroupInit(Map<String, String> args) {
|
||||
MuWireSettings settings = application.context.get("muwire-settings")
|
||||
@@ -92,6 +98,7 @@ class OptionsModel {
|
||||
clearUploads = uiSettings.clearUploads
|
||||
exitOnClose = uiSettings.exitOnClose
|
||||
storeSearchHistory = uiSettings.storeSearchHistory
|
||||
groupByFile = uiSettings.groupByFile
|
||||
|
||||
if (core.router != null) {
|
||||
inBw = String.valueOf(settings.inBw)
|
||||
@@ -102,5 +109,10 @@ class OptionsModel {
|
||||
searchExtraHop = settings.searchExtraHop
|
||||
trustLists = settings.allowTrustLists
|
||||
trustListInterval = String.valueOf(settings.trustListInterval)
|
||||
|
||||
startChatServer = settings.startChatServer
|
||||
maxChatConnections = settings.maxChatConnections
|
||||
advertiseChat = settings.advertiseChat
|
||||
maxChatLines = uiSettings.maxChatLines
|
||||
}
|
||||
}
|
@@ -24,6 +24,8 @@ class SearchTabModel {
|
||||
@Observable boolean browseActionEnabled
|
||||
@Observable boolean viewCommentActionEnabled
|
||||
@Observable boolean viewCertificatesActionEnabled
|
||||
@Observable boolean chatActionEnabled
|
||||
@Observable boolean groupedByFile
|
||||
|
||||
Core core
|
||||
UISettings uiSettings
|
||||
@@ -34,6 +36,9 @@ class SearchTabModel {
|
||||
def sourcesBucket = [:]
|
||||
def sendersBucket = new LinkedHashMap<>()
|
||||
|
||||
def results2 = []
|
||||
def senders2 = []
|
||||
|
||||
|
||||
void mvcGroupInit(Map<String, String> args) {
|
||||
core = mvcGroup.parentGroup.model.core
|
||||
@@ -72,9 +77,14 @@ class SearchTabModel {
|
||||
sourcesBucket.put(e.infohash, sourceBucket)
|
||||
}
|
||||
sourceBucket.addAll(e.sources)
|
||||
|
||||
results2.clear()
|
||||
results2.addAll(hashBucket.keySet())
|
||||
|
||||
JTable table = builder.getVariable("senders-table")
|
||||
table.model.fireTableDataChanged()
|
||||
table = builder.getVariable("results-table2")
|
||||
table.model.fireTableDataChanged()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,9 +117,16 @@ class SearchTabModel {
|
||||
|
||||
bucket << it
|
||||
senderBucket << it
|
||||
|
||||
}
|
||||
results2.clear()
|
||||
results2.addAll(hashBucket.keySet())
|
||||
JTable table = builder.getVariable("senders-table")
|
||||
table.model.fireTableDataChanged()
|
||||
table = builder.getVariable("results-table2")
|
||||
int selectedRow = table.getSelectedRow()
|
||||
table.model.fireTableDataChanged()
|
||||
table.selectionModel.setSelectionInterval(selectedRow, selectedRow)
|
||||
}
|
||||
}
|
||||
}
|
@@ -16,9 +16,11 @@ class SharedFileModel {
|
||||
def downloaders = []
|
||||
def certificates = []
|
||||
|
||||
@Observable boolean showCommentActionEnabled
|
||||
|
||||
public void mvcGroupInit(Map<String,String> args) {
|
||||
searchers.addAll(sf.searches)
|
||||
downloaders.addAll(sf.downloaders)
|
||||
certificates.addAll(core.certificateManager.byInfoHash.get(sf.infoHash))
|
||||
searchers.addAll(sf.getSearches())
|
||||
downloaders.addAll(sf.getDownloaders())
|
||||
certificates.addAll(core.certificateManager.byInfoHash.getOrDefault(sf.infoHash,[]))
|
||||
}
|
||||
}
|
@@ -39,6 +39,8 @@ class BrowseView {
|
||||
def p
|
||||
def resultsTable
|
||||
def lastSortEvent
|
||||
def sequentialDownloadCheckbox
|
||||
|
||||
void initUI() {
|
||||
int rowHeight = application.context.get("row-height")
|
||||
mainFrame = application.windowManager.findWindow("main-frame")
|
||||
@@ -67,6 +69,8 @@ class BrowseView {
|
||||
button(text : "View Comment", enabled : bind{model.viewCommentActionEnabled}, viewCommentAction)
|
||||
button(text : "View Certificates", enabled : bind{model.viewCertificatesActionEnabled}, viewCertificatesAction)
|
||||
button(text : "Dismiss", dismissAction)
|
||||
label(text : "Download sequentially")
|
||||
sequentialDownloadCheckbox = checkBox()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,8 +110,9 @@ class BrowseView {
|
||||
else
|
||||
model.viewCommentActionEnabled = false
|
||||
|
||||
def mainFrameGroup = application.mvcGroupManager.getGroups()['MainFrame']
|
||||
rows.each {
|
||||
downloadActionEnabled &= mvcGroup.parentGroup.parentGroup.model.canDownload(model.results[it].infohash)
|
||||
downloadActionEnabled &= mainFrameGroup.model.canDownload(model.results[it].infohash)
|
||||
}
|
||||
model.downloadActionEnabled = downloadActionEnabled
|
||||
|
||||
|
@@ -65,10 +65,7 @@ class CertificateControlView {
|
||||
closureColumn(header : "File Name", type : String, read : {it.name.name})
|
||||
closureColumn(header : "Hash", type : String, read : {Base64.encode(it.infoHash.getRoot())})
|
||||
closureColumn(header : "Comment", preferredWidth : 20, type : Boolean, read : {it.comment != null})
|
||||
closureColumn(header : "Timestamp", type : String, read : {
|
||||
def date = new Date(it.timestamp)
|
||||
date.toString()
|
||||
})
|
||||
closureColumn(header : "Timestamp", type : Long, read : { it.timestamp })
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -113,6 +110,8 @@ class CertificateControlView {
|
||||
}
|
||||
})
|
||||
|
||||
certsTable.setDefaultRenderer(Long.class, new DateRenderer())
|
||||
|
||||
dialog.getContentPane().add(panel)
|
||||
dialog.pack()
|
||||
dialog.setLocationRelativeTo(mainFrame)
|
||||
|
182
gui/griffon-app/views/com/muwire/gui/ChatRoomView.groovy
Normal file
182
gui/griffon-app/views/com/muwire/gui/ChatRoomView.groovy
Normal file
@@ -0,0 +1,182 @@
|
||||
package com.muwire.gui
|
||||
|
||||
import griffon.core.artifact.GriffonView
|
||||
import griffon.inject.MVCMember
|
||||
import griffon.metadata.ArtifactProviderFor
|
||||
|
||||
import javax.swing.JMenuItem
|
||||
import javax.swing.JPopupMenu
|
||||
import javax.swing.JSplitPane
|
||||
import javax.swing.ListSelectionModel
|
||||
import javax.swing.SwingConstants
|
||||
import javax.swing.SpringLayout.Constraints
|
||||
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.chat.ChatConnectionAttemptStatus
|
||||
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.event.MouseAdapter
|
||||
import java.awt.event.MouseEvent
|
||||
|
||||
import javax.annotation.Nonnull
|
||||
|
||||
@ArtifactProviderFor(GriffonView)
|
||||
class ChatRoomView {
|
||||
@MVCMember @Nonnull
|
||||
FactoryBuilderSupport builder
|
||||
@MVCMember @Nonnull
|
||||
ChatRoomModel model
|
||||
@MVCMember @Nonnull
|
||||
ChatRoomController controller
|
||||
|
||||
ChatNotificator chatNotificator
|
||||
|
||||
def pane
|
||||
def parent
|
||||
def sayField
|
||||
def roomTextArea
|
||||
def textScrollPane
|
||||
def membersTable
|
||||
def lastMembersTableSortEvent
|
||||
|
||||
void initUI() {
|
||||
int rowHeight = application.context.get("row-height")
|
||||
def parentModel = mvcGroup.parentGroup.model
|
||||
if (model.console || model.privateChat) {
|
||||
pane = builder.panel {
|
||||
borderLayout()
|
||||
panel(constraints : BorderLayout.CENTER) {
|
||||
gridLayout(rows : 1, cols : 1)
|
||||
textScrollPane = scrollPane {
|
||||
roomTextArea = textArea(editable : false, lineWrap : true, wrapStyleWord : true)
|
||||
}
|
||||
}
|
||||
panel(constraints : BorderLayout.SOUTH) {
|
||||
borderLayout()
|
||||
label(text : "Say something here: ", constraints : BorderLayout.WEST)
|
||||
sayField = textField(enabled : bind {parentModel.sayActionEnabled}, actionPerformed : {controller.say()}, constraints : BorderLayout.CENTER)
|
||||
button(enabled : bind {parentModel.sayActionEnabled},text : "Say", constraints : BorderLayout.EAST, sayAction)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
pane = builder.panel {
|
||||
borderLayout()
|
||||
panel(constraints : BorderLayout.CENTER) {
|
||||
gridLayout(rows : 1, cols : 1)
|
||||
splitPane(orientation : JSplitPane.HORIZONTAL_SPLIT, continuousLayout : true, dividerLocation : 300) {
|
||||
panel {
|
||||
gridLayout(rows : 1, cols : 1)
|
||||
scrollPane {
|
||||
membersTable = table(autoCreateRowSorter : true, rowHeight : rowHeight) {
|
||||
tableModel(list : model.members) {
|
||||
closureColumn(header : "Name", preferredWidth: 100, type: String, read : {it.getHumanReadableName()})
|
||||
closureColumn(header : "Trust Status", preferredWidth: 30, type : String, read : {String.valueOf(model.core.trustService.getLevel(it.destination))})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
panel {
|
||||
gridLayout(rows : 1, cols : 1)
|
||||
textScrollPane = scrollPane {
|
||||
roomTextArea = textArea(editable : false, lineWrap : true, wrapStyleWord : true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
panel(constraints : BorderLayout.SOUTH) {
|
||||
borderLayout()
|
||||
label(text : "Say something here: ", constraints : BorderLayout.WEST)
|
||||
sayField = textField(enabled : bind {parentModel.sayActionEnabled}, actionPerformed : {controller.say()}, constraints : BorderLayout.CENTER)
|
||||
button(enabled : bind {parentModel.sayActionEnabled}, text : "Say", constraints : BorderLayout.EAST, sayAction)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
SmartScroller smartScroller = new SmartScroller(textScrollPane)
|
||||
pane.putClientProperty("mvcId", mvcGroup.mvcId)
|
||||
}
|
||||
|
||||
void mvcGroupInit(Map<String,String> args) {
|
||||
parent = mvcGroup.parentGroup.view.builder.getVariable(model.tabName)
|
||||
parent.addTab(model.roomTabName, pane)
|
||||
|
||||
int index = parent.indexOfComponent(pane)
|
||||
parent.setSelectedIndex(index)
|
||||
|
||||
def tabPanel = builder.panel {
|
||||
borderLayout()
|
||||
panel (constraints : BorderLayout.CENTER) {
|
||||
label(text : model.roomTabName)
|
||||
}
|
||||
button(icon : imageIcon("/close_tab.png"), preferredSize: [20, 20], constraints : BorderLayout.EAST,
|
||||
actionPerformed : closeTab )
|
||||
}
|
||||
if (!model.console)
|
||||
parent.setTabComponentAt(index, tabPanel)
|
||||
|
||||
if (membersTable != null) {
|
||||
|
||||
membersTable.rowSorter.addRowSorterListener({evt -> lastMembersTableSortEvent = evt})
|
||||
membersTable.rowSorter.setSortsOnUpdates(true)
|
||||
membersTable.getSelectionModel().setSelectionMode(ListSelectionModel.SINGLE_SELECTION)
|
||||
|
||||
membersTable.addMouseListener(new MouseAdapter() {
|
||||
public void mouseClicked(MouseEvent e) {
|
||||
if (e.button == MouseEvent.BUTTON1 && e.clickCount > 1) {
|
||||
controller.privateMessage()
|
||||
} else if (e.isPopupTrigger() || e.button == MouseEvent.BUTTON3)
|
||||
showPopupMenu(e)
|
||||
}
|
||||
|
||||
public void mouseReleased(MouseEvent e) {
|
||||
if (e.isPopupTrigger() || e.button == MouseEvent.BUTTON3)
|
||||
showPopupMenu(e)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private void showPopupMenu(MouseEvent e) {
|
||||
JPopupMenu menu = new JPopupMenu()
|
||||
JMenuItem privateChat = new JMenuItem("Start Private Chat")
|
||||
privateChat.addActionListener({controller.privateMessage()})
|
||||
menu.add(privateChat)
|
||||
JMenuItem browse = new JMenuItem("Browse")
|
||||
browse.addActionListener({controller.browse()})
|
||||
menu.add(browse)
|
||||
JMenuItem markTrusted = new JMenuItem("Mark Trusted")
|
||||
markTrusted.addActionListener({controller.markTrusted()})
|
||||
menu.add(markTrusted)
|
||||
JMenuItem markNeutral = new JMenuItem("Mark Neutral")
|
||||
markNeutral.addActionListener({controller.markNeutral()})
|
||||
menu.add(markNeutral)
|
||||
JMenuItem markDistrusted = new JMenuItem("Mark Distrusted")
|
||||
markDistrusted.addActionListener({controller.markDistrusted()})
|
||||
menu.add(markDistrusted)
|
||||
menu.show(e.getComponent(), e.getX(), e.getY())
|
||||
}
|
||||
|
||||
Persona getSelectedPersona() {
|
||||
int selectedRow = membersTable.getSelectedRow()
|
||||
if (selectedRow < 0)
|
||||
return null
|
||||
if (lastMembersTableSortEvent != null)
|
||||
selectedRow = membersTable.rowSorter.convertRowIndexToModel(selectedRow)
|
||||
model.members[selectedRow]
|
||||
}
|
||||
|
||||
void refreshMembersTable() {
|
||||
int selectedRow = membersTable.getSelectedRow()
|
||||
membersTable.model.fireTableDataChanged()
|
||||
membersTable.selectionModel.setSelectionInterval(selectedRow, selectedRow)
|
||||
}
|
||||
|
||||
def closeTab = {
|
||||
int index = parent.indexOfComponent(pane)
|
||||
parent.removeTabAt(index)
|
||||
controller.leaveRoom()
|
||||
chatNotificator.roomClosed(mvcGroup.mvcId)
|
||||
mvcGroup.destroy()
|
||||
}
|
||||
}
|
89
gui/griffon-app/views/com/muwire/gui/ChatServerView.groovy
Normal file
89
gui/griffon-app/views/com/muwire/gui/ChatServerView.groovy
Normal file
@@ -0,0 +1,89 @@
|
||||
package com.muwire.gui
|
||||
|
||||
import griffon.core.artifact.GriffonView
|
||||
import griffon.inject.MVCMember
|
||||
import griffon.metadata.ArtifactProviderFor
|
||||
import javax.swing.SwingConstants
|
||||
|
||||
import com.muwire.core.chat.ChatServer
|
||||
|
||||
import java.awt.BorderLayout
|
||||
|
||||
import javax.annotation.Nonnull
|
||||
|
||||
@ArtifactProviderFor(GriffonView)
|
||||
class ChatServerView {
|
||||
@MVCMember @Nonnull
|
||||
FactoryBuilderSupport builder
|
||||
@MVCMember @Nonnull
|
||||
ChatServerModel model
|
||||
@MVCMember @Nonnull
|
||||
ChatServerController controller
|
||||
|
||||
ChatNotificator chatNotificator
|
||||
|
||||
def pane
|
||||
def parent
|
||||
def childPane
|
||||
|
||||
void initUI() {
|
||||
pane = builder.panel {
|
||||
borderLayout()
|
||||
childPane = tabbedPane(id : model.host.getHumanReadableName()+"-chat-rooms", constraints : BorderLayout.CENTER)
|
||||
panel(constraints : BorderLayout.SOUTH) {
|
||||
gridLayout(rows : 1, cols : 3)
|
||||
panel {}
|
||||
panel {
|
||||
button(text : bind {model.buttonText}, enabled : bind {model.disconnectActionEnabled}, disconnectAction)
|
||||
}
|
||||
panel {
|
||||
label(text : "Connection Status ")
|
||||
label(text : bind {model.status.toString()})
|
||||
}
|
||||
}
|
||||
}
|
||||
pane.putClientProperty("mvcId",mvcGroup.mvcId)
|
||||
pane.putClientProperty("childPane", childPane)
|
||||
childPane.addChangeListener({e -> chatNotificator.roomTabChanged(e.getSource())})
|
||||
}
|
||||
|
||||
void mvcGroupInit(Map<String,String> args) {
|
||||
parent = mvcGroup.parentGroup.view.builder.getVariable("chat-tabs")
|
||||
parent.addTab(model.host.getHumanReadableName(), pane)
|
||||
|
||||
int index = parent.indexOfComponent(pane)
|
||||
parent.setSelectedIndex(index)
|
||||
|
||||
def tabPanel
|
||||
builder.with {
|
||||
tabPanel = panel {
|
||||
borderLayout()
|
||||
panel (constraints : BorderLayout.CENTER) {
|
||||
String text = model.host == model.core.me ? "Local Server" : model.host.getHumanReadableName()
|
||||
label(text : text)
|
||||
}
|
||||
button(icon : imageIcon("/close_tab.png"), preferredSize: [20, 20], constraints : BorderLayout.EAST,
|
||||
actionPerformed : closeTab )
|
||||
}
|
||||
}
|
||||
parent.setTabComponentAt(index, tabPanel)
|
||||
|
||||
def params = [:]
|
||||
params['core'] = model.core
|
||||
params['tabName'] = model.host.getHumanReadableName() + "-chat-rooms"
|
||||
params['room'] = 'Console'
|
||||
params['roomTabName'] = 'Console'
|
||||
params['console'] = true
|
||||
params['host'] = model.host
|
||||
params['chatNotificator'] = chatNotificator
|
||||
mvcGroup.createMVCGroup("chat-room",model.host.getHumanReadableName()+"-"+ChatServer.CONSOLE, params)
|
||||
}
|
||||
|
||||
def closeTab = {
|
||||
if (model.buttonText == "Disconnect")
|
||||
controller.disconnect()
|
||||
int index = parent.indexOfComponent(pane)
|
||||
parent.removeTabAt(index)
|
||||
mvcGroup.destroy()
|
||||
}
|
||||
}
|
@@ -0,0 +1,61 @@
|
||||
package com.muwire.gui
|
||||
|
||||
import griffon.core.artifact.GriffonView
|
||||
import griffon.inject.MVCMember
|
||||
import griffon.metadata.ArtifactProviderFor
|
||||
|
||||
import javax.swing.Box
|
||||
import javax.swing.JDialog
|
||||
import javax.swing.JOptionPane
|
||||
import javax.swing.SwingConstants
|
||||
|
||||
import java.awt.event.WindowAdapter
|
||||
import java.awt.event.WindowEvent
|
||||
|
||||
import javax.annotation.Nonnull
|
||||
|
||||
@ArtifactProviderFor(GriffonView)
|
||||
class DownloadPreviewView {
|
||||
@MVCMember @Nonnull
|
||||
FactoryBuilderSupport builder
|
||||
@MVCMember @Nonnull
|
||||
DownloadPreviewModel model
|
||||
|
||||
def mainFrame
|
||||
def dialog
|
||||
def panel
|
||||
|
||||
void initUI() {
|
||||
mainFrame = application.windowManager.findWindow("main-frame")
|
||||
|
||||
dialog = new JDialog(mainFrame, "Generating Preview", true)
|
||||
|
||||
panel = builder.panel {
|
||||
vbox {
|
||||
label(text : "Generating preview for "+model.downloader.file.getName())
|
||||
Box.createVerticalGlue()
|
||||
progressBar(indeterminate : true)
|
||||
}
|
||||
}
|
||||
|
||||
dialog.getContentPane().add(panel)
|
||||
dialog.pack()
|
||||
dialog.setResizable(false)
|
||||
dialog.setLocationRelativeTo(mainFrame)
|
||||
dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE)
|
||||
dialog.addWindowListener(new WindowAdapter() {
|
||||
public void windowClosed(WindowEvent e) {
|
||||
mainFrame.setVisible(false)
|
||||
mvcGroup.destroy()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
void mvcGroupInit(Map<String, String> args) {
|
||||
if (!model.downloader.isSequential())
|
||||
JOptionPane.showMessageDialog(mainFrame, "This download is not sequential, there may not be much to preview")
|
||||
DownloadPreviewer previewer = new DownloadPreviewer(model.downloader, this)
|
||||
previewer.execute()
|
||||
dialog.show()
|
||||
}
|
||||
}
|
@@ -78,8 +78,10 @@ class MainFrameView {
|
||||
|
||||
|
||||
UISettings settings
|
||||
ChatNotificator chatNotificator
|
||||
|
||||
void initUI() {
|
||||
chatNotificator = new ChatNotificator(application.getMvcGroupManager())
|
||||
settings = application.context.get("ui-settings")
|
||||
int rowHeight = application.context.get("row-height")
|
||||
builder.with {
|
||||
@@ -142,6 +144,7 @@ class MainFrameView {
|
||||
if (settings.showMonitor)
|
||||
button(text: "Monitor", enabled: bind{model.monitorPaneButtonEnabled},actionPerformed : showMonitorWindow)
|
||||
button(text: "Trust", enabled:bind{model.trustPaneButtonEnabled},actionPerformed : showTrustWindow)
|
||||
button(text: "Chat", enabled : bind{model.chatPaneButtonEnabled}, actionPerformed : showChatWindow)
|
||||
}
|
||||
panel(id: "top-panel", constraints: BorderLayout.CENTER) {
|
||||
cardLayout()
|
||||
@@ -216,6 +219,7 @@ class MainFrameView {
|
||||
button(text: "Pause", enabled : bind {model.pauseButtonEnabled}, pauseAction)
|
||||
button(text: bind { model.resumeButtonText }, enabled : bind {model.retryButtonEnabled}, resumeAction)
|
||||
button(text: "Cancel", enabled : bind {model.cancelButtonEnabled }, cancelAction)
|
||||
button(text: "Preview", enabled : bind {model.previewButtonEnabled}, previewAction)
|
||||
button(text: "Clear Done", enabled : bind {model.clearButtonEnabled}, clearAction)
|
||||
}
|
||||
}
|
||||
@@ -304,7 +308,7 @@ class MainFrameView {
|
||||
radioButton(text : "Table", selected : false, buttonGroup : sharedViewType, actionPerformed : showSharedFilesTable)
|
||||
}
|
||||
panel {
|
||||
button(text : "Share files", actionPerformed : shareFiles)
|
||||
button(text : "Share", actionPerformed : shareFiles)
|
||||
button(text : "Add Comment", enabled : bind {model.addCommentButtonEnabled}, addCommentAction)
|
||||
button(text : "Certify", enabled : bind {model.addCommentButtonEnabled}, issueCertificateAction)
|
||||
}
|
||||
@@ -422,7 +426,8 @@ class MainFrameView {
|
||||
scrollPane(constraints : BorderLayout.CENTER) {
|
||||
table(id : "trusted-table", autoCreateRowSorter : true, rowHeight : rowHeight) {
|
||||
tableModel(list : model.trusted) {
|
||||
closureColumn(header : "Trusted Users", type : String, read : { it.getHumanReadableName() } )
|
||||
closureColumn(header : "Trusted Users", type : String, read : { it.persona.getHumanReadableName() } )
|
||||
closureColumn(header : "Reason", type : String, read : {it.reason})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -431,6 +436,8 @@ class MainFrameView {
|
||||
button(text : "Subscribe", enabled : bind {model.subscribeButtonEnabled}, constraints : gbc(gridx: 0, gridy : 0), subscribeAction)
|
||||
button(text : "Mark Neutral", enabled : bind {model.markNeutralFromTrustedButtonEnabled}, constraints : gbc(gridx: 1, gridy: 0), markNeutralFromTrustedAction)
|
||||
button(text : "Mark Distrusted", enabled : bind {model.markDistrustedButtonEnabled}, constraints : gbc(gridx: 2, gridy:0), markDistrustedAction)
|
||||
button(text : "Browse", enabled : bind{model.browseFromTrustedButtonEnabled}, constraints:gbc(gridx:3, gridy:0), browseFromTrustedAction)
|
||||
button(text : "Chat", enabled : bind{model.chatFromTrustedButtonEnabled} ,constraints : gbc(gridx:4, gridy:0), chatFromTrustedAction)
|
||||
}
|
||||
}
|
||||
panel (border : etchedBorder()){
|
||||
@@ -438,7 +445,8 @@ class MainFrameView {
|
||||
scrollPane(constraints : BorderLayout.CENTER) {
|
||||
table(id : "distrusted-table", autoCreateRowSorter : true, rowHeight : rowHeight) {
|
||||
tableModel(list : model.distrusted) {
|
||||
closureColumn(header: "Distrusted Users", type : String, read : { it.getHumanReadableName() } )
|
||||
closureColumn(header: "Distrusted Users", type : String, read : { it.persona.getHumanReadableName() } )
|
||||
closureColumn(header: "Reason", type : String, read : {it.reason})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -461,12 +469,7 @@ class MainFrameView {
|
||||
closureColumn(header : "Trusted", preferredWidth : 20, type: Integer, read : {it.good.size()})
|
||||
closureColumn(header : "Distrusted", preferredWidth: 20, type: Integer, read : {it.bad.size()})
|
||||
closureColumn(header : "Status", preferredWidth: 30, type: String, read : {it.status.toString()})
|
||||
closureColumn(header : "Last Updated", preferredWidth: 200, type : String, read : {
|
||||
if (it.timestamp == 0)
|
||||
return "Never"
|
||||
else
|
||||
return String.valueOf(new Date(it.timestamp))
|
||||
})
|
||||
closureColumn(header : "Last Updated", preferredWidth: 200, type : Long, read : { it.timestamp })
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -477,6 +480,15 @@ class MainFrameView {
|
||||
}
|
||||
}
|
||||
}
|
||||
panel(constraints : "chat window") {
|
||||
borderLayout()
|
||||
tabbedPane(id : "chat-tabs", constraints : BorderLayout.CENTER)
|
||||
panel(constraints : BorderLayout.SOUTH) {
|
||||
button(text : "Start Chat Server", enabled : bind {!model.chatServerRunning}, startChatServerAction)
|
||||
button(text : "Stop Chat Server", enabled : bind {model.chatServerRunning}, stopChatServerAction)
|
||||
button(text : "Connect To Remote Server", connectChatServerAction)
|
||||
}
|
||||
}
|
||||
}
|
||||
panel (border: etchedBorder(), constraints : BorderLayout.SOUTH) {
|
||||
borderLayout()
|
||||
@@ -501,7 +513,9 @@ class MainFrameView {
|
||||
public boolean importData(TransferHandler.TransferSupport support) {
|
||||
def files = support.getTransferable().getTransferData(DataFlavor.javaFileListFlavor)
|
||||
files.each {
|
||||
model.core.eventBus.publish(new FileSharedEvent(file : it))
|
||||
File canonical = it.getCanonicalFile()
|
||||
model.core.fileManager.negativeTree.remove(canonical)
|
||||
model.core.eventBus.publish(new FileSharedEvent(file : canonical))
|
||||
}
|
||||
showUploadsWindow.call()
|
||||
true
|
||||
@@ -510,6 +524,7 @@ class MainFrameView {
|
||||
|
||||
mainFrame.addWindowListener(new WindowAdapter(){
|
||||
public void windowClosing(WindowEvent e) {
|
||||
chatNotificator.mainWindowDeactivated()
|
||||
if (application.getContext().get("tray-icon")) {
|
||||
if (settings.closeWarning) {
|
||||
runInsideUIAsync {
|
||||
@@ -523,6 +538,13 @@ class MainFrameView {
|
||||
} else {
|
||||
closeApplication()
|
||||
}
|
||||
}
|
||||
public void windowDeactivated(WindowEvent e) {
|
||||
chatNotificator.mainWindowDeactivated()
|
||||
}
|
||||
public void windowActivated(WindowEvent e) {
|
||||
if (!model.chatPaneButtonEnabled)
|
||||
chatNotificator.mainWindowActivated()
|
||||
}})
|
||||
|
||||
// search field
|
||||
@@ -539,6 +561,7 @@ class MainFrameView {
|
||||
model.cancelButtonEnabled = false
|
||||
model.retryButtonEnabled = false
|
||||
model.pauseButtonEnabled = false
|
||||
model.previewButtonEnabled = false
|
||||
model.downloader = null
|
||||
downloadDetailsPanel.getLayout().show(downloadDetailsPanel,"select-download")
|
||||
return
|
||||
@@ -547,6 +570,7 @@ class MainFrameView {
|
||||
if (downloader == null)
|
||||
return
|
||||
model.downloader = downloader
|
||||
model.previewButtonEnabled = true
|
||||
downloadDetailsPanel.getLayout().show(downloadDetailsPanel,"download-selected")
|
||||
switch(downloader.getCurrentState()) {
|
||||
case Downloader.DownloadState.CONNECTING :
|
||||
@@ -611,6 +635,9 @@ class MainFrameView {
|
||||
JMenuItem certifySelectedFiles = new JMenuItem("Certify selected files")
|
||||
certifySelectedFiles.addActionListener({mvcGroup.controller.issueCertificate()})
|
||||
sharedFilesMenu.add(certifySelectedFiles)
|
||||
JMenuItem openContainingFolder = new JMenuItem("Open containing folder")
|
||||
openContainingFolder.addActionListener({mvcGroup.controller.openContainingFolder()})
|
||||
sharedFilesMenu.add(openContainingFolder)
|
||||
JMenuItem showFileDetails = new JMenuItem("Show file details")
|
||||
showFileDetails.addActionListener({mvcGroup.controller.showFileDetails()})
|
||||
sharedFilesMenu.add(showFileDetails)
|
||||
@@ -743,6 +770,8 @@ class MainFrameView {
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
subscriptionTable.setDefaultRenderer(Long.class, new DateRenderer())
|
||||
|
||||
// trusted table
|
||||
def trustedTable = builder.getVariable("trusted-table")
|
||||
@@ -756,10 +785,42 @@ class MainFrameView {
|
||||
model.subscribeButtonEnabled = false
|
||||
model.markDistrustedButtonEnabled = false
|
||||
model.markNeutralFromTrustedButtonEnabled = false
|
||||
model.chatFromTrustedButtonEnabled = false
|
||||
model.browseFromTrustedButtonEnabled = false
|
||||
} else {
|
||||
model.subscribeButtonEnabled = true
|
||||
model.markDistrustedButtonEnabled = true
|
||||
model.markNeutralFromTrustedButtonEnabled = true
|
||||
model.chatFromTrustedButtonEnabled = true
|
||||
model.browseFromTrustedButtonEnabled = true
|
||||
}
|
||||
})
|
||||
|
||||
JPopupMenu trustMenu = new JPopupMenu()
|
||||
JMenuItem subscribeItem = new JMenuItem("Subscribe")
|
||||
subscribeItem.addActionListener({mvcGroup.controller.subscribe()})
|
||||
trustMenu.add(subscribeItem)
|
||||
JMenuItem markNeutralItem = new JMenuItem("Mark Neutral")
|
||||
markNeutralItem.addActionListener({mvcGroup.controller.markNeutralFromTrusted()})
|
||||
trustMenu.add(markNeutralItem)
|
||||
JMenuItem markDistrustedItem = new JMenuItem("Mark Distrusted")
|
||||
markDistrustedItem.addActionListener({mvcGroup.controller.markDistrusted()})
|
||||
trustMenu.add(markDistrustedItem)
|
||||
JMenuItem browseItem = new JMenuItem("Browse")
|
||||
browseItem.addActionListener({mvcGroup.controller.browseFromTrusted()})
|
||||
trustMenu.add(browseItem)
|
||||
JMenuItem chatItem = new JMenuItem("Chat")
|
||||
chatItem.addActionListener({mvcGroup.controller.chatFromTrusted()})
|
||||
trustMenu.add(chatItem)
|
||||
|
||||
trustedTable.addMouseListener(new MouseAdapter() {
|
||||
public void mousePressed(MouseEvent e) {
|
||||
if (e.isPopupTrigger() || e.button == MouseEvent.BUTTON3)
|
||||
showPopupMenu(trustMenu, e)
|
||||
}
|
||||
public void mouseReleased(MouseEvent e) {
|
||||
if (e.isPopupTrigger() || e.button == MouseEvent.BUTTON3)
|
||||
showPopupMenu(trustMenu, e)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -780,6 +841,10 @@ class MainFrameView {
|
||||
}
|
||||
})
|
||||
|
||||
// chat tabs
|
||||
def chatTabbedPane = builder.getVariable("chat-tabs")
|
||||
chatTabbedPane.addChangeListener({e -> chatNotificator.serverTabChanged(e.getSource())})
|
||||
|
||||
// show tree by default
|
||||
showSharedFilesTree.call()
|
||||
|
||||
@@ -981,6 +1046,8 @@ class MainFrameView {
|
||||
model.uploadsPaneButtonEnabled = true
|
||||
model.monitorPaneButtonEnabled = true
|
||||
model.trustPaneButtonEnabled = true
|
||||
model.chatPaneButtonEnabled = true
|
||||
chatNotificator.mainWindowDeactivated()
|
||||
}
|
||||
|
||||
def showDownloadsWindow = {
|
||||
@@ -991,6 +1058,8 @@ class MainFrameView {
|
||||
model.uploadsPaneButtonEnabled = true
|
||||
model.monitorPaneButtonEnabled = true
|
||||
model.trustPaneButtonEnabled = true
|
||||
model.chatPaneButtonEnabled = true
|
||||
chatNotificator.mainWindowDeactivated()
|
||||
}
|
||||
|
||||
def showUploadsWindow = {
|
||||
@@ -1001,6 +1070,8 @@ class MainFrameView {
|
||||
model.uploadsPaneButtonEnabled = false
|
||||
model.monitorPaneButtonEnabled = true
|
||||
model.trustPaneButtonEnabled = true
|
||||
model.chatPaneButtonEnabled = true
|
||||
chatNotificator.mainWindowDeactivated()
|
||||
}
|
||||
|
||||
def showMonitorWindow = {
|
||||
@@ -1011,6 +1082,8 @@ class MainFrameView {
|
||||
model.uploadsPaneButtonEnabled = true
|
||||
model.monitorPaneButtonEnabled = false
|
||||
model.trustPaneButtonEnabled = true
|
||||
model.chatPaneButtonEnabled = true
|
||||
chatNotificator.mainWindowDeactivated()
|
||||
}
|
||||
|
||||
def showTrustWindow = {
|
||||
@@ -1021,6 +1094,20 @@ class MainFrameView {
|
||||
model.uploadsPaneButtonEnabled = true
|
||||
model.monitorPaneButtonEnabled = true
|
||||
model.trustPaneButtonEnabled = false
|
||||
model.chatPaneButtonEnabled = true
|
||||
chatNotificator.mainWindowDeactivated()
|
||||
}
|
||||
|
||||
def showChatWindow = {
|
||||
def cardsPanel = builder.getVariable("cards-panel")
|
||||
cardsPanel.getLayout().show(cardsPanel, "chat window")
|
||||
model.searchesPaneButtonEnabled = true
|
||||
model.downloadsPaneButtonEnabled = true
|
||||
model.uploadsPaneButtonEnabled = true
|
||||
model.monitorPaneButtonEnabled = true
|
||||
model.trustPaneButtonEnabled = true
|
||||
model.chatPaneButtonEnabled = false
|
||||
chatNotificator.mainWindowActivated()
|
||||
}
|
||||
|
||||
def showSharedFilesTable = {
|
||||
@@ -1038,13 +1125,14 @@ class MainFrameView {
|
||||
def shareFiles = {
|
||||
def chooser = new JFileChooser()
|
||||
chooser.setFileHidingEnabled(!model.core.muOptions.shareHiddenFiles)
|
||||
chooser.setDialogTitle("Select file to share")
|
||||
chooser.setFileSelectionMode(JFileChooser.FILES_ONLY)
|
||||
chooser.setDialogTitle("Select files or directories to share")
|
||||
chooser.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES)
|
||||
chooser.setMultiSelectionEnabled(true)
|
||||
int rv = chooser.showOpenDialog(null)
|
||||
if (rv == JFileChooser.APPROVE_OPTION) {
|
||||
chooser.getSelectedFiles().each {
|
||||
File canonical = it.getCanonicalFile()
|
||||
model.core.fileManager.negativeTree.remove(canonical)
|
||||
model.core.eventBus.publish(new FileSharedEvent(file : canonical))
|
||||
}
|
||||
}
|
||||
|
@@ -33,6 +33,7 @@ class OptionsView {
|
||||
def u
|
||||
def bandwidth
|
||||
def trust
|
||||
def chat
|
||||
|
||||
def retryField
|
||||
def updateField
|
||||
@@ -71,6 +72,11 @@ class OptionsView {
|
||||
def allowTrustListsCheckbox
|
||||
def trustListIntervalField
|
||||
|
||||
def startChatServerCheckbox
|
||||
def maxChatConnectionsField
|
||||
def advertiseChatCheckbox
|
||||
def maxChatLinesField
|
||||
|
||||
def buttonsPanel
|
||||
|
||||
def mainFrame
|
||||
@@ -200,10 +206,16 @@ class OptionsView {
|
||||
panel (border : titledBorder(title : "Search Settings", border : etchedBorder(), titlePosition : TitledBorder.TOP),
|
||||
constraints : gbc(gridx : 0, gridy : 1, fill : GridBagConstraints.HORIZONTAL, weightx : 100)) {
|
||||
gridBagLayout()
|
||||
label(text : "Remember search history", constraints: gbc(gridx: 0, gridy:0, anchor : GridBagConstraints.LINE_START, weightx: 100))
|
||||
label(text : "By default, group search results by", constraints : gbc(gridx :0, gridy: 0, anchor : GridBagConstraints.LINE_START, weightx : 100))
|
||||
panel(constraints : gbc(gridx : 1, gridy : 0, anchor : GridBagConstraints.LINE_END)) {
|
||||
buttonGroup(id : "groupBy")
|
||||
radioButton(text : "Sender", selected : bind {!model.groupByFile}, buttonGroup : groupBy, groupBySenderAction)
|
||||
radioButton(text : "File", selected : bind {model.groupByFile}, buttonGroup : groupBy, groupByFileAction)
|
||||
}
|
||||
label(text : "Remember search history", constraints: gbc(gridx: 0, gridy:1, anchor : GridBagConstraints.LINE_START, weightx: 100))
|
||||
storeSearchHistoryCheckbox = checkBox(selected : bind {model.storeSearchHistory},
|
||||
constraints : gbc(gridx : 1, gridy:0, anchor : GridBagConstraints.LINE_END))
|
||||
button(text : "Clear history", constraints : gbc(gridx : 1, gridy : 1, anchor : GridBagConstraints.LINE_END), clearHistoryAction)
|
||||
constraints : gbc(gridx : 1, gridy:1, anchor : GridBagConstraints.LINE_END))
|
||||
button(text : "Clear history", constraints : gbc(gridx : 1, gridy : 2, anchor : GridBagConstraints.LINE_END), clearHistoryAction)
|
||||
|
||||
}
|
||||
panel (border : titledBorder(title : "Other Settings", border : etchedBorder(), titlePosition : TitledBorder.TOP),
|
||||
@@ -221,7 +233,7 @@ class OptionsView {
|
||||
label(text : "Exclude local files from results", constraints: gbc(gridx:0, gridy:3, anchor : GridBagConstraints.LINE_START, weightx: 100))
|
||||
excludeLocalResultCheckbox = checkBox(selected : bind {model.excludeLocalResult},
|
||||
constraints : gbc(gridx: 1, gridy : 3, anchor : GridBagConstraints.LINE_END))
|
||||
label(text : "Automatically Clear finished uploads", constraints:gbc(gridx:0, gridy:4, anchor: GridBagConstraints.LINE_START, weightx : 100))
|
||||
label(text : "Automatically clear finished uploads", constraints:gbc(gridx:0, gridy:4, anchor: GridBagConstraints.LINE_START, weightx : 100))
|
||||
clearUploadsCheckbox = checkBox(selected : bind {model.clearUploads},
|
||||
constraints : gbc(gridx:1, gridy: 4, anchor:GridBagConstraints.LINE_END))
|
||||
label(text : "When closing MuWire", constraints : gbc(gridx: 0, gridy : 5, anchor : GridBagConstraints.LINE_START, weightx: 100))
|
||||
@@ -261,6 +273,23 @@ class OptionsView {
|
||||
}
|
||||
panel(constraints : gbc(gridx: 0, gridy : 1, weighty: 100))
|
||||
}
|
||||
|
||||
chat = builder.panel {
|
||||
gridBagLayout()
|
||||
panel (border : titledBorder(title : "Chat Settings", border : etchedBorder(), titlePosition : TitledBorder.TOP),
|
||||
constraints : gbc(gridx : 0, gridy : 0, fill : GridBagConstraints.HORIZONTAL, weightx: 100)) {
|
||||
gridBagLayout()
|
||||
label(text : "Start chat server on startup", constraints : gbc(gridx: 0, gridy: 0, anchor: GridBagConstraints.LINE_START, weightx: 100))
|
||||
startChatServerCheckbox = checkBox(selected : bind{model.startChatServer}, constraints : gbc(gridx:1, gridy:0, anchor:GridBagConstraints.LINE_END))
|
||||
label(text : "Maximum chat connections (-1 means unlimited)", constraints : gbc(gridx: 0, gridy:1, anchor:GridBagConstraints.LINE_START, weightx:100))
|
||||
maxChatConnectionsField = textField(text : bind {model.maxChatConnections}, constraints : gbc(gridx: 1, gridy : 1, anchor:GridBagConstraints.LINE_END))
|
||||
label(text : "Advertise chat ability in search results", constraints : gbc(gridx: 0, gridy:2, anchor:GridBagConstraints.LINE_START, weightx:100))
|
||||
advertiseChatCheckbox = checkBox(selected : bind{model.advertiseChat}, constraints : gbc(gridx:1, gridy:2, anchor:GridBagConstraints.LINE_END))
|
||||
label(text : "Maximum lines of scrollback (-1 means unlimited)", constraints : gbc(gridx:0, gridy:3, anchor : GridBagConstraints.LINE_START, weightx: 100))
|
||||
maxChatLinesField = textField(text : bind{model.maxChatLines}, constraints : gbc(gridx:1, gridy: 3, anchor: GridBagConstraints.LINE_END))
|
||||
}
|
||||
panel(constraints : gbc(gridx: 0, gridy : 1, weighty: 100))
|
||||
}
|
||||
|
||||
|
||||
buttonsPanel = builder.panel {
|
||||
@@ -280,6 +309,7 @@ class OptionsView {
|
||||
tabbedPane.addTab("Bandwidth", bandwidth)
|
||||
}
|
||||
tabbedPane.addTab("Trust", trust)
|
||||
tabbedPane.addTab("Chat", chat)
|
||||
|
||||
JPanel panel = new JPanel()
|
||||
panel.setLayout(new BorderLayout())
|
||||
|
@@ -17,6 +17,7 @@ import javax.swing.ListSelectionModel
|
||||
import javax.swing.SwingConstants
|
||||
import javax.swing.table.DefaultTableCellRenderer
|
||||
|
||||
import com.muwire.core.InfoHash
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.search.UIResultEvent
|
||||
import com.muwire.core.util.DataUtil
|
||||
@@ -38,77 +39,187 @@ class SearchTabView {
|
||||
FactoryBuilderSupport builder
|
||||
@MVCMember @Nonnull
|
||||
SearchTabModel model
|
||||
|
||||
UISettings settings
|
||||
|
||||
def pane
|
||||
def parent
|
||||
def searchTerms
|
||||
def sendersTable
|
||||
def sendersTable, sendersTable2
|
||||
def lastSendersSortEvent
|
||||
def resultsTable
|
||||
def resultsTable, resultsTable2
|
||||
def lastSortEvent
|
||||
def lastResults2SortEvent, lastSenders2SortEvent
|
||||
def sequentialDownloadCheckbox
|
||||
def sequentialDownloadCheckbox2
|
||||
|
||||
void initUI() {
|
||||
int rowHeight = application.context.get("row-height")
|
||||
builder.with {
|
||||
def resultsTable
|
||||
def sendersTable
|
||||
def sequentialDownloadCheckbox
|
||||
def resultsTable, resultsTable2
|
||||
def sendersTable, sendersTable2
|
||||
def sequentialDownloadCheckbox, sequentialDownloadCheckbox2
|
||||
def pane = panel {
|
||||
gridLayout(rows :1, cols : 1)
|
||||
splitPane(orientation: JSplitPane.VERTICAL_SPLIT, continuousLayout : true, dividerLocation: 300 ) {
|
||||
panel {
|
||||
borderLayout()
|
||||
scrollPane (constraints : BorderLayout.CENTER) {
|
||||
sendersTable = table(id : "senders-table", autoCreateRowSorter : true, rowHeight : rowHeight) {
|
||||
tableModel(list : model.senders) {
|
||||
closureColumn(header : "Sender", preferredWidth : 500, type: String, read : {row -> row.getHumanReadableName()})
|
||||
closureColumn(header : "Results", preferredWidth : 20, type: Integer, read : {row -> model.sendersBucket[row].size()})
|
||||
closureColumn(header : "Browse", preferredWidth : 20, type: Boolean, read : {row -> model.sendersBucket[row].first().browse})
|
||||
closureColumn(header : "Trust", preferredWidth : 50, type: String, read : { row ->
|
||||
model.core.trustService.getLevel(row.destination).toString()
|
||||
})
|
||||
borderLayout()
|
||||
panel (id : "results-panel", constraints : BorderLayout.CENTER) {
|
||||
cardLayout()
|
||||
panel (constraints : "grouped-by-sender"){
|
||||
gridLayout(rows :1, cols : 1)
|
||||
splitPane(orientation: JSplitPane.VERTICAL_SPLIT, continuousLayout : true, dividerLocation: 300 ) {
|
||||
panel {
|
||||
borderLayout()
|
||||
scrollPane (constraints : BorderLayout.CENTER) {
|
||||
sendersTable = table(id : "senders-table", autoCreateRowSorter : true, rowHeight : rowHeight) {
|
||||
tableModel(list : model.senders) {
|
||||
closureColumn(header : "Sender", preferredWidth : 500, type: String, read : {row -> row.getHumanReadableName()})
|
||||
closureColumn(header : "Results", preferredWidth : 20, type: Integer, read : {row -> model.sendersBucket[row].size()})
|
||||
closureColumn(header : "Browse", preferredWidth : 20, type: Boolean, read : {row -> model.sendersBucket[row].first().browse})
|
||||
closureColumn(header : "Chat", preferredWidth : 20, type : Boolean, read : {row -> model.sendersBucket[row].first().chat})
|
||||
closureColumn(header : "Trust", preferredWidth : 50, type: String, read : { row ->
|
||||
model.core.trustService.getLevel(row.destination).toString()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
panel(constraints : BorderLayout.SOUTH) {
|
||||
gridLayout(rows: 1, cols : 2)
|
||||
panel (border : etchedBorder()){
|
||||
button(text : "Browse Host", enabled : bind {model.browseActionEnabled}, browseAction)
|
||||
button(text : "Chat", enabled : bind{model.chatActionEnabled}, chatAction)
|
||||
}
|
||||
panel (border : etchedBorder()){
|
||||
button(text : "Trust", enabled: bind {model.trustButtonsEnabled }, trustAction)
|
||||
button(text : "Neutral", enabled: bind {model.trustButtonsEnabled}, neutralAction)
|
||||
button(text : "Distrust", enabled : bind {model.trustButtonsEnabled}, distrustAction)
|
||||
}
|
||||
}
|
||||
}
|
||||
panel {
|
||||
borderLayout()
|
||||
scrollPane (constraints : BorderLayout.CENTER) {
|
||||
resultsTable = table(id : "results-table", autoCreateRowSorter : true, rowHeight : rowHeight) {
|
||||
tableModel(list: model.results) {
|
||||
closureColumn(header: "Name", preferredWidth: 350, type: String, read : {row -> row.name.replace('<','_')})
|
||||
closureColumn(header: "Size", preferredWidth: 20, type: Long, read : {row -> row.size})
|
||||
closureColumn(header: "Direct Sources", preferredWidth: 50, type : Integer, read : { row -> model.hashBucket[row.infohash].size()})
|
||||
closureColumn(header: "Possible Sources", preferredWidth : 50, type : Integer, read : {row -> model.sourcesBucket[row.infohash].size()})
|
||||
closureColumn(header: "Comments", preferredWidth: 20, type: Boolean, read : {row -> row.comment != null})
|
||||
closureColumn(header: "Certificates", preferredWidth: 20, type: Integer, read : {row -> row.certificates})
|
||||
}
|
||||
}
|
||||
}
|
||||
panel(constraints : BorderLayout.SOUTH) {
|
||||
gridBagLayout()
|
||||
label(text : "", constraints : gbc(gridx : 0, gridy: 0, weightx : 100))
|
||||
button(text : "Download", enabled : bind {model.downloadActionEnabled}, constraints : gbc(gridx : 1, gridy:0), downloadAction)
|
||||
button(text : "View Comment", enabled : bind {model.viewCommentActionEnabled}, constraints : gbc(gridx:2, gridy:0), showCommentAction)
|
||||
button(text : "View Certificates", enabled : bind {model.viewCertificatesActionEnabled}, constraints : gbc(gridx:3, gridy:0), viewCertificatesAction)
|
||||
label(text : "Download sequentially", constraints : gbc(gridx: 4, gridy: 0, weightx : 80, anchor : GridBagConstraints.LINE_END))
|
||||
sequentialDownloadCheckbox = checkBox(constraints : gbc(gridx : 5, gridy: 0, anchor : GridBagConstraints.LINE_END),
|
||||
selected : false, enabled : bind {model.downloadActionEnabled})
|
||||
}
|
||||
}
|
||||
}
|
||||
panel(constraints : BorderLayout.SOUTH) {
|
||||
gridLayout(rows: 1, cols : 2)
|
||||
panel (border : etchedBorder()){
|
||||
button(text : "Browse Host", enabled : bind {model.browseActionEnabled}, browseAction)
|
||||
}
|
||||
panel (border : etchedBorder()){
|
||||
button(text : "Trust", enabled: bind {model.trustButtonsEnabled }, trustAction)
|
||||
button(text : "Neutral", enabled: bind {model.trustButtonsEnabled}, neutralAction)
|
||||
button(text : "Distrust", enabled : bind {model.trustButtonsEnabled}, distrustAction)
|
||||
}
|
||||
}
|
||||
}
|
||||
panel {
|
||||
borderLayout()
|
||||
scrollPane (constraints : BorderLayout.CENTER) {
|
||||
resultsTable = table(id : "results-table", autoCreateRowSorter : true, rowHeight : rowHeight) {
|
||||
tableModel(list: model.results) {
|
||||
closureColumn(header: "Name", preferredWidth: 350, type: String, read : {row -> row.name.replace('<','_')})
|
||||
closureColumn(header: "Size", preferredWidth: 20, type: Long, read : {row -> row.size})
|
||||
closureColumn(header: "Direct Sources", preferredWidth: 50, type : Integer, read : { row -> model.hashBucket[row.infohash].size()})
|
||||
closureColumn(header: "Possible Sources", preferredWidth : 50, type : Integer, read : {row -> model.sourcesBucket[row.infohash].size()})
|
||||
closureColumn(header: "Comments", preferredWidth: 20, type: Boolean, read : {row -> row.comment != null})
|
||||
closureColumn(header: "Certificates", preferredWidth: 20, type: Integer, read : {row -> row.certificates})
|
||||
panel (constraints : "grouped-by-file") {
|
||||
gridLayout(rows : 1, cols : 1)
|
||||
splitPane(orientation: JSplitPane.VERTICAL_SPLIT, continuousLayout : true, dividerLocation: 300 ) {
|
||||
panel {
|
||||
borderLayout()
|
||||
scrollPane(constraints : BorderLayout.CENTER) {
|
||||
resultsTable2 = table(id : "results-table2", autoCreateRowSorter : true, rowHeight : rowHeight) {
|
||||
tableModel(list : model.results2) {
|
||||
closureColumn(header : "Name", preferredWidth : 350, type : String, read : {
|
||||
model.hashBucket[it].first().name.replace('<', '_')
|
||||
})
|
||||
closureColumn(header : "Size", preferredWidth : 20, type : Long, read : {
|
||||
model.hashBucket[it].first().size
|
||||
})
|
||||
closureColumn(header : "Direct Sources", preferredWidth : 20, type : Integer, read : {
|
||||
model.hashBucket[it].size()
|
||||
})
|
||||
closureColumn(header : "Possible Sources", preferredWidth : 20, type : Integer , read : {
|
||||
model.sourcesBucket[it].size()
|
||||
})
|
||||
closureColumn(header : "Comments", preferredWidth : 20, type : Integer, read : {
|
||||
int count = 0
|
||||
model.hashBucket[it].each {
|
||||
if (it.comment != null)
|
||||
count++
|
||||
}
|
||||
count
|
||||
})
|
||||
closureColumn(header : "Certificates", preferredWidth : 20, type : Integer, read : {
|
||||
int count = 0
|
||||
model.hashBucket[it].each {
|
||||
count += it.certificates
|
||||
}
|
||||
count
|
||||
})
|
||||
closureColumn(header : "Chat Hosts", preferredWidth : 20, type : Integer, read : {
|
||||
int count = 0
|
||||
model.hashBucket[it].each {
|
||||
if (it.chat)
|
||||
count++
|
||||
}
|
||||
count
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
panel (constraints : BorderLayout.SOUTH) {
|
||||
gridLayout(rows :1, cols : 3)
|
||||
panel {}
|
||||
panel {
|
||||
button(text : "Download", enabled : bind {model.downloadActionEnabled}, downloadAction)
|
||||
}
|
||||
panel {
|
||||
gridBagLayout()
|
||||
label(text : "Download sequentially", constraints : gbc(gridx : 0, gridy : 0, weightx : 100, anchor : GridBagConstraints.LINE_END))
|
||||
sequentialDownloadCheckbox2 = checkBox( constraints : gbc(gridx: 1, gridy:0, weightx: 0, anchor : GridBagConstraints.LINE_END))
|
||||
}
|
||||
}
|
||||
}
|
||||
panel {
|
||||
borderLayout()
|
||||
scrollPane(constraints : BorderLayout.CENTER) {
|
||||
sendersTable2 = table(id : "senders-table2", autoCreateRowSorter : true, rowHeight : rowHeight) {
|
||||
tableModel(list : model.senders2) {
|
||||
closureColumn(header : "Sender", preferredWidth : 350, type : String, read : {it.sender.getHumanReadableName()})
|
||||
closureColumn(header : "Browse", preferredWidth : 20, type : Boolean, read : {it.browse})
|
||||
closureColumn(header : "Chat", preferredWidth : 20, type : Boolean, read : {it.chat})
|
||||
closureColumn(header : "Comment", preferredWidth : 20, type : Boolean, read : {it.comment != null})
|
||||
closureColumn(header : "Certificates", preferredWidth : 20, type: Integer, read : {it.certificates})
|
||||
closureColumn(header : "Trust", preferredWidth : 50, type : String, read : {
|
||||
model.core.trustService.getLevel(it.sender.destination).toString()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
panel (constraints : BorderLayout.SOUTH) {
|
||||
gridLayout(rows : 1, cols : 2)
|
||||
panel (border : etchedBorder()) {
|
||||
button(text : "Browse Host", enabled : bind {model.browseActionEnabled}, browseAction)
|
||||
button(text : "Chat", enabled : bind{model.chatActionEnabled}, chatAction)
|
||||
button(text : "View Comment", enabled : bind {model.viewCommentActionEnabled}, showCommentAction)
|
||||
button(text : "View Certificates", enabled : bind {model.viewCertificatesActionEnabled}, viewCertificatesAction)
|
||||
}
|
||||
panel (border : etchedBorder()) {
|
||||
button(text : "Trust", enabled: bind {model.trustButtonsEnabled }, trustAction)
|
||||
button(text : "Neutral", enabled: bind {model.trustButtonsEnabled}, neutralAction)
|
||||
button(text : "Distrust", enabled : bind {model.trustButtonsEnabled}, distrustAction)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
panel(constraints : BorderLayout.SOUTH) {
|
||||
gridBagLayout()
|
||||
label(text : "", constraints : gbc(gridx : 0, gridy: 0, weightx : 100))
|
||||
button(text : "Download", enabled : bind {model.downloadActionEnabled}, constraints : gbc(gridx : 1, gridy:0), downloadAction)
|
||||
button(text : "View Comment", enabled : bind {model.viewCommentActionEnabled}, constraints : gbc(gridx:2, gridy:0), showCommentAction)
|
||||
button(text : "View Certificates", enabled : bind {model.viewCertificatesActionEnabled}, constraints : gbc(gridx:3, gridy:0), viewCertificatesAction)
|
||||
label(text : "Download sequentially", constraints : gbc(gridx: 4, gridy: 0, weightx : 80, anchor : GridBagConstraints.LINE_END))
|
||||
sequentialDownloadCheckbox = checkBox(constraints : gbc(gridx : 5, gridy: 0, anchor : GridBagConstraints.LINE_END),
|
||||
selected : false, enabled : bind {model.downloadActionEnabled})
|
||||
}
|
||||
}
|
||||
}
|
||||
panel (constraints : BorderLayout.SOUTH) {
|
||||
label(text : "Group by")
|
||||
buttonGroup(id : "groupBy")
|
||||
radioButton(text : "Sender", selected : bind {!model.groupedByFile}, buttonGroup : groupBy, actionPerformed: showSenderGrouping)
|
||||
radioButton(text : "File", selected : bind {model.groupedByFile}, buttonGroup : groupBy, actionPerformed: showFileGrouping)
|
||||
}
|
||||
}
|
||||
|
||||
this.pane = pane
|
||||
@@ -117,7 +228,10 @@ class SearchTabView {
|
||||
|
||||
this.resultsTable = resultsTable
|
||||
this.sendersTable = sendersTable
|
||||
this.resultsTable2 = resultsTable2
|
||||
this.sendersTable2 = sendersTable2
|
||||
this.sequentialDownloadCheckbox = sequentialDownloadCheckbox
|
||||
this.sequentialDownloadCheckbox2 = sequentialDownloadCheckbox2
|
||||
|
||||
def selectionModel = resultsTable.getSelectionModel()
|
||||
selectionModel.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION)
|
||||
@@ -212,17 +326,82 @@ class SearchTabView {
|
||||
if (row < 0) {
|
||||
model.trustButtonsEnabled = false
|
||||
model.browseActionEnabled = false
|
||||
model.chatActionEnabled = false
|
||||
return
|
||||
} else {
|
||||
Persona sender = model.senders[row]
|
||||
model.browseActionEnabled = model.sendersBucket[sender].first().browse
|
||||
model.chatActionEnabled = model.sendersBucket[sender].first().chat
|
||||
model.trustButtonsEnabled = true
|
||||
model.results.clear()
|
||||
model.results.addAll(model.sendersBucket[sender])
|
||||
resultsTable.model.fireTableDataChanged()
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
// results table 2
|
||||
resultsTable2.setDefaultRenderer(Integer.class,centerRenderer)
|
||||
resultsTable2.columnModel.getColumn(1).setCellRenderer(new SizeRenderer())
|
||||
resultsTable2.rowSorter.addRowSorterListener({evt -> lastResults2SortEvent = evt})
|
||||
resultsTable2.rowSorter.setSortsOnUpdates(true)
|
||||
selectionModel = resultsTable2.getSelectionModel()
|
||||
selectionModel.setSelectionMode(ListSelectionModel.SINGLE_SELECTION)
|
||||
selectionModel.addListSelectionListener({
|
||||
UIResultEvent e = getSelectedResult()
|
||||
if (e == null) {
|
||||
model.trustButtonsEnabled = false
|
||||
model.browseActionEnabled = false
|
||||
model.chatActionEnabled = false
|
||||
model.viewCertificatesActionEnabled = false
|
||||
return
|
||||
}
|
||||
model.downloadActionEnabled = true
|
||||
def results = model.hashBucket[e.infohash]
|
||||
model.senders2.clear()
|
||||
model.senders2.addAll(results)
|
||||
int selectedRow = sendersTable2.getSelectedRow()
|
||||
sendersTable2.model.fireTableDataChanged()
|
||||
if (selectedRow < results.size())
|
||||
sendersTable2.selectionModel.setSelectionInterval(selectedRow,selectedRow)
|
||||
})
|
||||
|
||||
resultsTable2.addMouseListener(new MouseAdapter() {
|
||||
@Override
|
||||
public void mouseClicked(MouseEvent e) {
|
||||
if (e.getClickCount() > 1 && e.button == MouseEvent.BUTTON1)
|
||||
mvcGroup.controller.download()
|
||||
}
|
||||
})
|
||||
|
||||
// TODO: add download right-click action
|
||||
|
||||
// senders table 2
|
||||
sendersTable2.setDefaultRenderer(Integer.class, centerRenderer)
|
||||
sendersTable2.rowSorter.addRowSorterListener({ evt -> lastSenders2SortEvent = evt})
|
||||
sendersTable2.rowSorter.setSortsOnUpdates(true)
|
||||
selectionModel = sendersTable2.getSelectionModel()
|
||||
selectionModel.setSelectionMode(ListSelectionModel.SINGLE_SELECTION)
|
||||
selectionModel.addListSelectionListener({
|
||||
int row = selectedSenderRow()
|
||||
if (row < 0 || model.senders2[row] == null) {
|
||||
model.browseActionEnabled = false
|
||||
model.chatActionEnabled = false
|
||||
model.viewCertificatesActionEnabled = false
|
||||
model.trustButtonsEnabled = false
|
||||
model.viewCommentActionEnabled = false
|
||||
return
|
||||
}
|
||||
model.browseActionEnabled = model.senders2[row].browse
|
||||
model.chatActionEnabled = model.senders2[row].chat
|
||||
model.trustButtonsEnabled = true
|
||||
model.viewCommentActionEnabled = model.senders2[row].comment != null
|
||||
model.viewCertificatesActionEnabled = model.senders2[row].certificates > 0
|
||||
})
|
||||
|
||||
if (settings.groupByFile)
|
||||
showFileGrouping.call()
|
||||
else
|
||||
showSenderGrouping.call()
|
||||
}
|
||||
|
||||
def closeTab = {
|
||||
@@ -269,14 +448,35 @@ class SearchTabView {
|
||||
menu.show(e.getComponent(), e.getX(), e.getY())
|
||||
}
|
||||
|
||||
private UIResultEvent getSelectedResult() {
|
||||
int[] selectedRows = resultsTable.getSelectedRows()
|
||||
if (selectedRows.length != 1)
|
||||
return null
|
||||
int selected = selectedRows[0]
|
||||
if (lastSortEvent != null)
|
||||
selected = resultsTable.rowSorter.convertRowIndexToModel(selected)
|
||||
model.results[selected]
|
||||
UIResultEvent getSelectedResult() {
|
||||
if (model.groupedByFile) {
|
||||
int selectedRow = resultsTable2.getSelectedRow()
|
||||
if (selectedRow < 0)
|
||||
return null
|
||||
if (lastResults2SortEvent != null)
|
||||
selectedRow = resultsTable2.rowSorter.convertRowIndexToModel(selectedRow)
|
||||
InfoHash infohash = model.results2[selectedRow]
|
||||
|
||||
Persona sender = selectedSender()
|
||||
if (sender == null) // really shouldn't happen
|
||||
return model.hashBucket[infohash].first()
|
||||
|
||||
for (UIResultEvent candidate : model.hashBucket[infohash]) {
|
||||
if (candidate.sender == sender)
|
||||
return candidate
|
||||
}
|
||||
|
||||
// also shouldn't happen
|
||||
return model.hashBucket[infohash].first()
|
||||
} else {
|
||||
int[] selectedRows = resultsTable.getSelectedRows()
|
||||
if (selectedRows.length != 1)
|
||||
return null
|
||||
int selected = selectedRows[0]
|
||||
if (lastSortEvent != null)
|
||||
selected = resultsTable.rowSorter.convertRowIndexToModel(selected)
|
||||
return model.results[selected]
|
||||
}
|
||||
}
|
||||
|
||||
def copyHashToClipboard() {
|
||||
@@ -299,11 +499,49 @@ class SearchTabView {
|
||||
}
|
||||
|
||||
int selectedSenderRow() {
|
||||
int row = sendersTable.getSelectedRow()
|
||||
if (model.groupedByFile) {
|
||||
int row = sendersTable2.getSelectedRow()
|
||||
if (row < 0)
|
||||
return row
|
||||
if (lastSenders2SortEvent != null)
|
||||
row = sendersTable2.rowSorter.convertRowIndexToModel(row)
|
||||
return row
|
||||
} else {
|
||||
int row = sendersTable.getSelectedRow()
|
||||
if (row < 0)
|
||||
return -1
|
||||
if (lastSendersSortEvent != null)
|
||||
row = sendersTable.rowSorter.convertRowIndexToModel(row)
|
||||
return row
|
||||
}
|
||||
}
|
||||
|
||||
Persona selectedSender() {
|
||||
int row = selectedSenderRow()
|
||||
if (row < 0)
|
||||
return -1
|
||||
if (lastSendersSortEvent != null)
|
||||
row = sendersTable.rowSorter.convertRowIndexToModel(row)
|
||||
row
|
||||
return null
|
||||
if (model.groupedByFile)
|
||||
return model.senders2[row]?.sender
|
||||
else
|
||||
return model.senders[row]
|
||||
}
|
||||
|
||||
def showSenderGrouping = {
|
||||
model.groupedByFile = false
|
||||
def cardsPanel = builder.getVariable("results-panel")
|
||||
cardsPanel.getLayout().show(cardsPanel, "grouped-by-sender")
|
||||
}
|
||||
|
||||
def showFileGrouping = {
|
||||
model.groupedByFile = true
|
||||
def cardsPanel = builder.getVariable("results-panel")
|
||||
cardsPanel.getLayout().show(cardsPanel, "grouped-by-file")
|
||||
}
|
||||
|
||||
boolean sequentialDownload() {
|
||||
if (model.groupedByFile)
|
||||
return sequentialDownloadCheckbox2.model.isSelected()
|
||||
else
|
||||
return sequentialDownloadCheckbox.model.isSelected()
|
||||
}
|
||||
}
|
@@ -5,10 +5,17 @@ import griffon.inject.MVCMember
|
||||
import griffon.metadata.ArtifactProviderFor
|
||||
|
||||
import javax.swing.JDialog
|
||||
import javax.swing.JMenuItem
|
||||
import javax.swing.JPopupMenu
|
||||
import javax.swing.JTabbedPane
|
||||
import javax.swing.ListSelectionModel
|
||||
import javax.swing.SwingConstants
|
||||
|
||||
import com.muwire.core.filecert.Certificate
|
||||
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.event.MouseAdapter
|
||||
import java.awt.event.MouseEvent
|
||||
import java.awt.event.WindowAdapter
|
||||
import java.awt.event.WindowEvent
|
||||
|
||||
@@ -20,14 +27,18 @@ class SharedFileView {
|
||||
FactoryBuilderSupport builder
|
||||
@MVCMember @Nonnull
|
||||
SharedFileModel model
|
||||
@MVCMember @Nonnull
|
||||
SharedFileController controller
|
||||
|
||||
def mainFrame
|
||||
def dialog
|
||||
def panel
|
||||
def searchersPanel
|
||||
def searchersTable
|
||||
def downloadersPanel
|
||||
def certificatesTable
|
||||
def certificatesPanel
|
||||
def lastCertificateSortEvent
|
||||
|
||||
void initUI() {
|
||||
mainFrame = application.windowManager.findWindow("main-frame")
|
||||
@@ -38,14 +49,11 @@ class SharedFileView {
|
||||
searchersPanel = builder.panel {
|
||||
borderLayout()
|
||||
scrollPane(constraints : BorderLayout.CENTER) {
|
||||
table(autoCreateRowSorter : true, rowHeight : rowHeight) {
|
||||
searchersTable = table(autoCreateRowSorter : true, rowHeight : rowHeight) {
|
||||
tableModel(list : model.searchers) {
|
||||
closureColumn(header : "Searcher", type : String, read : {it.searcher.getHumanReadableName()})
|
||||
closureColumn(header : "Searcher", type : String, read : {it.searcher?.getHumanReadableName()})
|
||||
closureColumn(header : "Query", type : String, read : {it.query})
|
||||
closureColumn(header : "Timestamp", type : String, read : {
|
||||
Date d = new Date(it.timestamp)
|
||||
d.toString()
|
||||
})
|
||||
closureColumn(header : "Timestamp", type : Long, read : {it.timestamp})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -70,17 +78,42 @@ class SharedFileView {
|
||||
closureColumn(header : "Issuer", type:String, read : {it.issuer.getHumanReadableName()})
|
||||
closureColumn(header : "File Name", type : String, read : {it.name.name})
|
||||
closureColumn(header : "Comment", type : Boolean, read : {it.comment != null})
|
||||
closureColumn(header : "Timestamp", type : String, read : {
|
||||
Date d = new Date(it.timestamp)
|
||||
d.toString()
|
||||
})
|
||||
closureColumn(header : "Timestamp", type : Long, read : {it.timestamp})
|
||||
}
|
||||
}
|
||||
}
|
||||
panel(constraints : BorderLayout.SOUTH) {
|
||||
button(text : "Show Comment", enabled : bind {model.showCommentActionEnabled}, showCommentAction)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void mvcGroupInit(Map<String,String> args) {
|
||||
|
||||
certificatesTable.rowSorter.addRowSorterListener({evt -> lastCertificateSortEvent = evt})
|
||||
def selectionModel = certificatesTable.getSelectionModel()
|
||||
selectionModel.setSelectionMode(ListSelectionModel.SINGLE_SELECTION)
|
||||
selectionModel.addListSelectionListener({
|
||||
Certificate c = getSelectedCertificate()
|
||||
model.showCommentActionEnabled = c != null && c.comment != null
|
||||
})
|
||||
|
||||
certificatesTable.addMouseListener(new MouseAdapter() {
|
||||
public void mouseReleased(MouseEvent e) {
|
||||
if (e.isPopupTrigger())
|
||||
showMenu(e)
|
||||
}
|
||||
public void mousePressed(MouseEvent e) {
|
||||
if (e.isPopupTrigger())
|
||||
showMenu(e)
|
||||
}
|
||||
})
|
||||
|
||||
certificatesTable.setDefaultRenderer(Long.class, new DateRenderer())
|
||||
|
||||
|
||||
searchersTable.setDefaultRenderer(Long.class, new DateRenderer())
|
||||
|
||||
def tabbedPane = new JTabbedPane()
|
||||
tabbedPane.addTab("Search Hits", searchersPanel)
|
||||
tabbedPane.addTab("Downloaders", downloadersPanel)
|
||||
@@ -99,4 +132,23 @@ class SharedFileView {
|
||||
show()
|
||||
}
|
||||
}
|
||||
|
||||
Certificate getSelectedCertificate() {
|
||||
int selectedRow = certificatesTable.getSelectedRow()
|
||||
if (selectedRow < 0)
|
||||
return null
|
||||
if (lastCertificateSortEvent != null)
|
||||
selectedRow = certificatesTable.rowSorter.convertRowIndexToModel(selectedRow)
|
||||
model.certificates[selectedRow]
|
||||
}
|
||||
|
||||
private void showMenu(MouseEvent e) {
|
||||
if (!model.showCommentActionEnabled)
|
||||
return
|
||||
JPopupMenu menu = new JPopupMenu()
|
||||
JMenuItem showComment = new JMenuItem("Show Comment")
|
||||
showComment.addActionListener({controller.showComment()})
|
||||
menu.add(showComment)
|
||||
menu.show(e.getComponent(), e.getX(), e.getY())
|
||||
}
|
||||
}
|
@@ -49,8 +49,9 @@ class TrustListView {
|
||||
scrollPane (constraints : BorderLayout.CENTER){
|
||||
table(id : "trusted-table", autoCreateRowSorter : true, rowHeight : rowHeight) {
|
||||
tableModel(list : model.trusted) {
|
||||
closureColumn(header: "Trusted Users", type : String, read : {it.getHumanReadableName()})
|
||||
closureColumn(header: "Your Trust", type : String, read : {model.trustService.getLevel(it.destination).toString()})
|
||||
closureColumn(header: "Trusted Users", type : String, read : {it.persona.getHumanReadableName()})
|
||||
closureColumn(header: "Reason", type : String, read : {it.reason})
|
||||
closureColumn(header: "Your Trust", type : String, read : {model.trustService.getLevel(it.persona.destination).toString()})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -65,8 +66,9 @@ class TrustListView {
|
||||
scrollPane (constraints : BorderLayout.CENTER ){
|
||||
table(id : "distrusted-table", autoCreateRowSorter : true, rowHeight : rowHeight) {
|
||||
tableModel(list : model.distrusted) {
|
||||
closureColumn(header: "Distrusted Users", type : String, read : {it.getHumanReadableName()})
|
||||
closureColumn(header: "Your Trust", type : String, read : {model.trustService.getLevel(it.destination).toString()})
|
||||
closureColumn(header: "Distrusted Users", type : String, read : {it.persona.getHumanReadableName()})
|
||||
closureColumn(header: "Reason", type:String, read : {it.reason})
|
||||
closureColumn(header: "Your Trust", type : String, read : {model.trustService.getLevel(it.persona.destination).toString()})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
91
gui/src/main/groovy/com/muwire/gui/ChatNotificator.groovy
Normal file
91
gui/src/main/groovy/com/muwire/gui/ChatNotificator.groovy
Normal file
@@ -0,0 +1,91 @@
|
||||
package com.muwire.gui
|
||||
|
||||
import java.awt.Taskbar
|
||||
import java.awt.Taskbar.Feature
|
||||
|
||||
import javax.swing.JPanel
|
||||
import javax.swing.JTabbedPane
|
||||
|
||||
import griffon.core.mvc.MVCGroupManager
|
||||
|
||||
class ChatNotificator {
|
||||
|
||||
private final MVCGroupManager groupManager
|
||||
|
||||
private boolean chatInFocus
|
||||
private String currentServerTab
|
||||
private String currentRoomTab
|
||||
|
||||
private final Map<String, Integer> roomsWithMessages = new HashMap<>()
|
||||
|
||||
ChatNotificator(MVCGroupManager groupManager) {
|
||||
this.groupManager = groupManager
|
||||
}
|
||||
|
||||
void serverTabChanged(JTabbedPane source) {
|
||||
JPanel panel = source.getSelectedComponent()
|
||||
String mvcId = panel.getClientProperty("mvcId")
|
||||
def group = groupManager.getGroups().get(mvcId)
|
||||
JTabbedPane childPane = panel.getClientProperty("childPane")
|
||||
JPanel roomPanel = childPane.getSelectedComponent()
|
||||
|
||||
currentServerTab = mvcId
|
||||
currentRoomTab = childPane.getSelectedComponent()?.getClientProperty("mvcId")
|
||||
|
||||
if (currentRoomTab != null) {
|
||||
roomsWithMessages.remove(currentRoomTab)
|
||||
updateBadge()
|
||||
}
|
||||
}
|
||||
|
||||
void roomTabChanged(JTabbedPane source) {
|
||||
JPanel panel = source.getSelectedComponent()
|
||||
currentRoomTab = panel.getClientProperty("mvcId")
|
||||
roomsWithMessages.remove(currentRoomTab)
|
||||
updateBadge()
|
||||
}
|
||||
|
||||
void roomClosed(String mvcId) {
|
||||
roomsWithMessages.remove(mvcId)
|
||||
updateBadge()
|
||||
}
|
||||
|
||||
void mainWindowDeactivated() {
|
||||
chatInFocus = false
|
||||
}
|
||||
|
||||
void mainWindowActivated() {
|
||||
chatInFocus = true
|
||||
if (currentRoomTab != null)
|
||||
roomsWithMessages.remove(currentRoomTab)
|
||||
updateBadge()
|
||||
}
|
||||
|
||||
void onMessage(String roomId) {
|
||||
if (roomId != currentRoomTab || !chatInFocus) {
|
||||
Integer previous = roomsWithMessages[roomId]
|
||||
if (previous == null)
|
||||
roomsWithMessages[roomId] = 1
|
||||
else
|
||||
roomsWithMessages[roomId] = previous + 1
|
||||
}
|
||||
updateBadge()
|
||||
}
|
||||
|
||||
private void updateBadge() {
|
||||
if (!Taskbar.isTaskbarSupported())
|
||||
return
|
||||
def taskBar = Taskbar.getTaskbar()
|
||||
if (!taskBar.isSupported(Feature.ICON_BADGE_NUMBER))
|
||||
return
|
||||
if (roomsWithMessages.isEmpty())
|
||||
taskBar.setIconBadge("")
|
||||
else {
|
||||
int total = 0
|
||||
roomsWithMessages.values().each {
|
||||
total += it
|
||||
}
|
||||
taskBar.setIconBadge(String.valueOf(total))
|
||||
}
|
||||
}
|
||||
}
|
33
gui/src/main/groovy/com/muwire/gui/DateRenderer.groovy
Normal file
33
gui/src/main/groovy/com/muwire/gui/DateRenderer.groovy
Normal file
@@ -0,0 +1,33 @@
|
||||
package com.muwire.gui
|
||||
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.JLabel
|
||||
import javax.swing.JTable
|
||||
import javax.swing.table.DefaultTableCellRenderer
|
||||
|
||||
import net.i2p.data.DataHelper
|
||||
|
||||
class DateRenderer extends DefaultTableCellRenderer {
|
||||
DateRenderer(){
|
||||
setHorizontalAlignment(JLabel.CENTER)
|
||||
}
|
||||
|
||||
JComponent getTableCellRendererComponent(JTable table, Object value,
|
||||
boolean isSelected, boolean hasFocus, int row, int column) {
|
||||
Long l = (Long) value
|
||||
String formatted
|
||||
if (l == 0)
|
||||
formatted = "Never"
|
||||
else
|
||||
formatted = DataHelper.formatTime(l)
|
||||
setText(formatted)
|
||||
if (isSelected) {
|
||||
setForeground(table.getSelectionForeground())
|
||||
setBackground(table.getSelectionBackground())
|
||||
} else {
|
||||
setForeground(table.getForeground())
|
||||
setBackground(table.getBackground())
|
||||
}
|
||||
this
|
||||
}
|
||||
}
|
36
gui/src/main/groovy/com/muwire/gui/DownloadPreviewer.groovy
Normal file
36
gui/src/main/groovy/com/muwire/gui/DownloadPreviewer.groovy
Normal file
@@ -0,0 +1,36 @@
|
||||
package com.muwire.gui
|
||||
|
||||
import java.awt.Desktop
|
||||
|
||||
import javax.swing.JDialog
|
||||
import javax.swing.JOptionPane
|
||||
import javax.swing.SwingWorker
|
||||
|
||||
import com.muwire.core.download.Downloader
|
||||
|
||||
class DownloadPreviewer extends SwingWorker {
|
||||
|
||||
private final Downloader downloader
|
||||
private final DownloadPreviewView view
|
||||
|
||||
DownloadPreviewer(Downloader downloader, DownloadPreviewView view) {
|
||||
this.downloader = downloader
|
||||
this.view = view
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object doInBackground() throws Exception {
|
||||
downloader.generatePreview()
|
||||
}
|
||||
|
||||
@Override
|
||||
public void done() {
|
||||
File previewFile = get()
|
||||
view.dialog.setVisible(false)
|
||||
view.mvcGroup.destroy()
|
||||
if (previewFile == null)
|
||||
JOptionPane.showMessageDialog(null, "Generating preview file failed", "Preview Failed", JOptionPane.ERROR_MESSAGE)
|
||||
else
|
||||
Desktop.getDesktop().open(previewFile)
|
||||
}
|
||||
}
|
@@ -14,6 +14,10 @@ class SizeRenderer extends DefaultTableCellRenderer {
|
||||
@Override
|
||||
JComponent getTableCellRendererComponent(JTable table, Object value,
|
||||
boolean isSelected, boolean hasFocus, int row, int column) {
|
||||
if (value == null) {
|
||||
// this is very strange, but it happens. Probably a swing bug?
|
||||
return super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column)
|
||||
}
|
||||
Long l = (Long) value
|
||||
String formatted = DataHelper.formatSize2Decimal(l, false)+"B"
|
||||
setText(formatted)
|
||||
|
@@ -18,6 +18,8 @@ class UISettings {
|
||||
boolean exitOnClose
|
||||
boolean clearUploads
|
||||
boolean storeSearchHistory
|
||||
boolean groupByFile
|
||||
int maxChatLines
|
||||
Set<String> searchHistory
|
||||
Set<String> openTabs
|
||||
|
||||
@@ -36,6 +38,8 @@ class UISettings {
|
||||
exitOnClose = Boolean.parseBoolean(props.getProperty("exitOnClose","false"))
|
||||
clearUploads = Boolean.parseBoolean(props.getProperty("clearUploads","false"))
|
||||
storeSearchHistory = Boolean.parseBoolean(props.getProperty("storeSearchHistory","true"))
|
||||
groupByFile = Boolean.parseBoolean(props.getProperty("groupByFile","false"))
|
||||
maxChatLines = Integer.parseInt(props.getProperty("maxChatLines","-1"))
|
||||
|
||||
searchHistory = DataUtil.readEncodedSet(props, "searchHistory")
|
||||
openTabs = DataUtil.readEncodedSet(props, "openTabs")
|
||||
@@ -56,6 +60,8 @@ class UISettings {
|
||||
props.setProperty("exitOnClose", String.valueOf(exitOnClose))
|
||||
props.setProperty("clearUploads", String.valueOf(clearUploads))
|
||||
props.setProperty("storeSearchHistory", String.valueOf(storeSearchHistory))
|
||||
props.setProperty("groupByFile", String.valueOf(groupByFile))
|
||||
props.setProperty("maxChatLines", String.valueOf(maxChatLines))
|
||||
if (font != null)
|
||||
props.setProperty("font", font)
|
||||
|
||||
|
174
gui/src/main/java/com/muwire/gui/SmartScroller.java
Normal file
174
gui/src/main/java/com/muwire/gui/SmartScroller.java
Normal file
@@ -0,0 +1,174 @@
|
||||
package com.muwire.gui;
|
||||
|
||||
import java.awt.Component;
|
||||
import java.awt.event.*;
|
||||
import javax.swing.*;
|
||||
import javax.swing.text.*;
|
||||
|
||||
/**
|
||||
* The SmartScroller will attempt to keep the viewport positioned based on
|
||||
* the users interaction with the scrollbar. The normal behaviour is to keep
|
||||
* the viewport positioned to see new data as it is dynamically added.
|
||||
*
|
||||
* Assuming vertical scrolling and data is added to the bottom:
|
||||
*
|
||||
* - when the viewport is at the bottom and new data is added,
|
||||
* then automatically scroll the viewport to the bottom
|
||||
* - when the viewport is not at the bottom and new data is added,
|
||||
* then do nothing with the viewport
|
||||
*
|
||||
* Assuming vertical scrolling and data is added to the top:
|
||||
*
|
||||
* - when the viewport is at the top and new data is added,
|
||||
* then do nothing with the viewport
|
||||
* - when the viewport is not at the top and new data is added, then adjust
|
||||
* the viewport to the relative position it was at before the data was added
|
||||
*
|
||||
* Similiar logic would apply for horizontal scrolling.
|
||||
*/
|
||||
public class SmartScroller implements AdjustmentListener
|
||||
{
|
||||
public final static int HORIZONTAL = 0;
|
||||
public final static int VERTICAL = 1;
|
||||
|
||||
public final static int START = 0;
|
||||
public final static int END = 1;
|
||||
|
||||
private int viewportPosition;
|
||||
|
||||
private JScrollBar scrollBar;
|
||||
private boolean adjustScrollBar = true;
|
||||
|
||||
private int previousValue = -1;
|
||||
private int previousMaximum = -1;
|
||||
|
||||
/**
|
||||
* Convenience constructor.
|
||||
* Scroll direction is VERTICAL and viewport position is at the END.
|
||||
*
|
||||
* @param scrollPane the scroll pane to monitor
|
||||
*/
|
||||
public SmartScroller(JScrollPane scrollPane)
|
||||
{
|
||||
this(scrollPane, VERTICAL, END);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience constructor.
|
||||
* Scroll direction is VERTICAL.
|
||||
*
|
||||
* @param scrollPane the scroll pane to monitor
|
||||
* @param viewportPosition valid values are START and END
|
||||
*/
|
||||
public SmartScroller(JScrollPane scrollPane, int viewportPosition)
|
||||
{
|
||||
this(scrollPane, VERTICAL, viewportPosition);
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify how the SmartScroller will function.
|
||||
*
|
||||
* @param scrollPane the scroll pane to monitor
|
||||
* @param scrollDirection indicates which JScrollBar to monitor.
|
||||
* Valid values are HORIZONTAL and VERTICAL.
|
||||
* @param viewportPosition indicates where the viewport will normally be
|
||||
* positioned as data is added.
|
||||
* Valid values are START and END
|
||||
*/
|
||||
public SmartScroller(JScrollPane scrollPane, int scrollDirection, int viewportPosition)
|
||||
{
|
||||
if (scrollDirection != HORIZONTAL
|
||||
&& scrollDirection != VERTICAL)
|
||||
throw new IllegalArgumentException("invalid scroll direction specified");
|
||||
|
||||
if (viewportPosition != START
|
||||
&& viewportPosition != END)
|
||||
throw new IllegalArgumentException("invalid viewport position specified");
|
||||
|
||||
this.viewportPosition = viewportPosition;
|
||||
|
||||
if (scrollDirection == HORIZONTAL)
|
||||
scrollBar = scrollPane.getHorizontalScrollBar();
|
||||
else
|
||||
scrollBar = scrollPane.getVerticalScrollBar();
|
||||
|
||||
scrollBar.addAdjustmentListener( this );
|
||||
|
||||
// Turn off automatic scrolling for text components
|
||||
|
||||
Component view = scrollPane.getViewport().getView();
|
||||
|
||||
if (view instanceof JTextComponent)
|
||||
{
|
||||
JTextComponent textComponent = (JTextComponent)view;
|
||||
DefaultCaret caret = (DefaultCaret)textComponent.getCaret();
|
||||
caret.setUpdatePolicy(DefaultCaret.NEVER_UPDATE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void adjustmentValueChanged(final AdjustmentEvent e)
|
||||
{
|
||||
SwingUtilities.invokeLater(new Runnable()
|
||||
{
|
||||
public void run()
|
||||
{
|
||||
checkScrollBar(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
* Analyze every adjustment event to determine when the viewport
|
||||
* needs to be repositioned.
|
||||
*/
|
||||
private void checkScrollBar(AdjustmentEvent e)
|
||||
{
|
||||
// The scroll bar listModel contains information needed to determine
|
||||
// whether the viewport should be repositioned or not.
|
||||
|
||||
JScrollBar scrollBar = (JScrollBar)e.getSource();
|
||||
BoundedRangeModel listModel = scrollBar.getModel();
|
||||
int value = listModel.getValue();
|
||||
int extent = listModel.getExtent();
|
||||
int maximum = listModel.getMaximum();
|
||||
|
||||
boolean valueChanged = previousValue != value;
|
||||
boolean maximumChanged = previousMaximum != maximum;
|
||||
|
||||
// Check if the user has manually repositioned the scrollbar
|
||||
|
||||
if (valueChanged && !maximumChanged)
|
||||
{
|
||||
if (viewportPosition == START)
|
||||
adjustScrollBar = value != 0;
|
||||
else
|
||||
adjustScrollBar = value + extent >= maximum;
|
||||
}
|
||||
|
||||
// Reset the "value" so we can reposition the viewport and
|
||||
// distinguish between a user scroll and a program scroll.
|
||||
// (ie. valueChanged will be false on a program scroll)
|
||||
|
||||
if (adjustScrollBar && viewportPosition == END)
|
||||
{
|
||||
// Scroll the viewport to the end.
|
||||
scrollBar.removeAdjustmentListener( this );
|
||||
value = maximum - extent;
|
||||
scrollBar.setValue( value );
|
||||
scrollBar.addAdjustmentListener( this );
|
||||
}
|
||||
|
||||
if (adjustScrollBar && viewportPosition == START)
|
||||
{
|
||||
// Keep the viewport at the same relative viewportPosition
|
||||
scrollBar.removeAdjustmentListener( this );
|
||||
value = value + maximum - previousMaximum;
|
||||
scrollBar.setValue( value );
|
||||
scrollBar.addAdjustmentListener( this );
|
||||
}
|
||||
|
||||
previousValue = value;
|
||||
previousMaximum = maximum;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user