Compare commits

..

30 Commits

Author SHA1 Message Date
Zlatin Balevsky
0c40c8f269 Release 0.6.6 2019-11-16 17:12:21 +00:00
Zlatin Balevsky
681ddb99a2 add ability to say something in a room 2019-11-16 17:10:21 +00:00
Zlatin Balevsky
5dff319746 prevent starting chat server more than once. Implement chat console in the cli 2019-11-16 16:40:07 +00:00
Zlatin Balevsky
57c4a00ac6 do not call disconnect() unless connecting/ed. This prevents trying to connect after closing the tab 2019-11-16 15:25:23 +00:00
Zlatin Balevsky
286a0a8678 redo locking around chat client state 2019-11-16 14:41:47 +00:00
Zlatin Balevsky
17eff7d77f toString 2019-11-16 00:25:50 +00:00
Zlatin Balevsky
2e22369ce0 set flag before submitting to threadpool 2019-11-15 23:12:17 +00:00
Zlatin Balevsky
15c59b440f flush outputstream on chat connect failures or rejections 2019-11-15 21:52:35 +00:00
Zlatin Balevsky
8fb015acbf sort browse host as well 2019-11-15 14:59:52 +00:00
Zlatin Balevsky
f7b11c90fd sort results from search 2019-11-15 14:49:15 +00:00
Zlatin Balevsky
df93a35062 wip on sorting 2019-11-15 14:23:00 +00:00
Zlatin Balevsky
ecb19a8412 do not update badge if the room is a console 2019-11-15 13:50:13 +00:00
Zlatin Balevsky
b1e5b40800 update minimum JDK to 9, version bump 2019-11-15 13:18:34 +00:00
Zlatin Balevsky
daa3a293f2 new messages update taskbar badge 2019-11-15 13:15:27 +00:00
Zlatin Balevsky
907264fc67 enable/disable chat and browse from trusted pane buttons 2019-11-15 02:15:56 +00:00
Zlatin Balevsky
c6becb93dc enable/disable say field when not connected 2019-11-15 01:57:48 +00:00
Zlatin Balevsky
2954bd2f1a smart scrolling the chat text area 2019-11-15 01:44:54 +00:00
Zlatin Balevsky
35322d2c15 fetch group by name,add sequential download checkbox to browse view 2019-11-14 12:40:40 +00:00
Zlatin Balevsky
9f6a7eb368 make sure browse window works from every parent group 2019-11-14 11:04:38 +00:00
Zlatin Balevsky
fec81808e5 Release 0.6.5 2019-11-14 05:15:26 +00:00
Zlatin Balevsky
4db890484d do not rejoin console 2019-11-14 04:49:13 +00:00
Zlatin Balevsky
dfd5e06889 add browse ability from chat room view 2019-11-14 04:40:15 +00:00
Zlatin Balevsky
71da8e14da name button earlier 2019-11-14 04:25:45 +00:00
Zlatin Balevsky
7dc37e3e0d change button to connect/disconnect 2019-11-14 04:20:57 +00:00
Zlatin Balevsky
3de058a078 send rejoins to the console pt2 2019-11-14 03:59:01 +00:00
Zlatin Balevsky
4d70c7adce send rejoins to the console 2019-11-14 03:58:36 +00:00
Zlatin Balevsky
5b41106476 start and stop poller thread on events 2019-11-14 03:45:21 +00:00
Zlatin Balevsky
6240b22e66 fix reconnecting to server, start with fresh member list upon rejoin 2019-11-14 03:13:01 +00:00
Zlatin Balevsky
0e26f5afd7 rejoin rooms on reconnect 2019-11-14 02:40:22 +00:00
Zlatin Balevsky
114bc06dbb If the user explicitly shares a file, remove it form the negative tree. #26 2019-11-13 22:00:10 +00:00
35 changed files with 920 additions and 76 deletions

View File

@@ -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. It is inspired by the LimeWire Gnutella client and developped by a former LimeWire developer.
The current stable release - 0.6.2 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 ### 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 ./gradlew clean assemble

View File

@@ -72,4 +72,27 @@ class BrowseModel {
void setPercentageLabel(Label percentage) { void setPercentageLabel(Label percentage) {
this.percentage = 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)
}
}
} }

View File

@@ -58,11 +58,17 @@ class BrowseView extends BasicWindow {
} }
contentPanel.addComponent(table, layoutData) contentPanel.addComponent(table, layoutData)
Panel buttonsPanel = new Panel()
buttonsPanel.setLayoutManager(new GridLayout(2))
Button sortButton = new Button("Sort...", {sort()})
Button closeButton = new Button("Close",{ Button closeButton = new Button("Close",{
model.unregister() model.unregister()
close() close()
}) })
contentPanel.addComponent(closeButton, layoutData) buttonsPanel.addComponent(sortButton, layoutData)
buttonsPanel.addComponent(closeButton, layoutData)
contentPanel.addComponent(buttonsPanel, layoutData)
setComponent(contentPanel) setComponent(contentPanel)
} }
@@ -120,4 +126,11 @@ class BrowseView extends BasicWindow {
ViewCertificatesView view = new ViewCertificatesView(model, textGUI, core, terminalSize) ViewCertificatesView view = new ViewCertificatesView(model, textGUI, core, terminalSize)
textGUI.addWindowAndWait(view) textGUI.addWindowAndWait(view)
} }
private void sort() {
SortPrompt prompt = new SortPrompt(textGUI)
SortType type = prompt.prompt()
if (type != null)
model.sort(type)
}
} }

View File

@@ -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")})
}
}

View File

@@ -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)
}
}

View File

@@ -32,7 +32,7 @@ import com.muwire.core.UILoadedEvent
import com.muwire.core.files.AllFilesLoadedEvent import com.muwire.core.files.AllFilesLoadedEvent
class CliLanterna { class CliLanterna {
private static final String MW_VERSION = "0.6.4" private static final String MW_VERSION = "0.6.6"
private static volatile Core core private static volatile Core core

View File

@@ -78,4 +78,32 @@ class FilesModel {
model.addRow(new SharedFileWrapper(it), DataHelper.formatSize2(size, false)+"B", comment, certified, hits, downloaders) 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()
} }

View File

@@ -51,17 +51,19 @@ class FilesView extends BasicWindow {
contentPanel.addComponent(table, layoutData) contentPanel.addComponent(table, layoutData)
Panel buttonsPanel = new Panel() Panel buttonsPanel = new Panel()
buttonsPanel.setLayoutManager(new GridLayout(4)) buttonsPanel.setLayoutManager(new GridLayout(5))
Button shareFile = new Button("Share File", {shareFile()}) Button shareFile = new Button("Share File", {shareFile()})
Button shareDirectory = new Button("Share Directory", {shareDirectory()}) Button shareDirectory = new Button("Share Directory", {shareDirectory()})
Button unshareDirectory = new Button("Unshare Directory",{unshareDirectory()}) Button unshareDirectory = new Button("Unshare Directory",{unshareDirectory()})
Button sort = new Button("Sort...",{sort()})
Button close = new Button("Close", {close()}) Button close = new Button("Close", {close()})
buttonsPanel.with { buttonsPanel.with {
addComponent(shareFile, layoutData) addComponent(shareFile, layoutData)
addComponent(shareDirectory, layoutData) addComponent(shareDirectory, layoutData)
addComponent(unshareDirectory, layoutData) addComponent(unshareDirectory, layoutData)
addComponent(sort, layoutData)
addComponent(close, layoutData) addComponent(close, layoutData)
} }
@@ -134,4 +136,11 @@ class FilesView extends BasicWindow {
core.eventBus.publish(new DirectoryUnsharedEvent(directory : directory)) core.eventBus.publish(new DirectoryUnsharedEvent(directory : directory))
MessageDialog.showMessageDialog(textGUI, "Directory Unshared", directory.getName()+" has been unshared", MessageDialogButton.OK) 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)
}
} }

View File

@@ -44,6 +44,7 @@ class MainWindowView extends BasicWindow {
private final UploadsModel uploadsModel private final UploadsModel uploadsModel
private final FilesModel filesModel private final FilesModel filesModel
private final TrustModel trustModel private final TrustModel trustModel
private final ChatConsoleModel chatModel
private final Label connectionCount, incoming, outgoing private final Label connectionCount, incoming, outgoing
private final Label known, failing, hopeless private final Label known, failing, hopeless
@@ -63,6 +64,9 @@ class MainWindowView extends BasicWindow {
uploadsModel = new UploadsModel(textGUI.getGUIThread(), core, props) uploadsModel = new UploadsModel(textGUI.getGUIThread(), core, props)
filesModel = new FilesModel(textGUI.getGUIThread(),core) filesModel = new FilesModel(textGUI.getGUIThread(),core)
trustModel = new TrustModel(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]) setHints([Window.Hint.EXPANDED])
Panel contentPanel = new Panel() Panel contentPanel = new Panel()
@@ -74,7 +78,7 @@ class MainWindowView extends BasicWindow {
Panel buttonsPanel = new Panel() Panel buttonsPanel = new Panel()
contentPanel.addComponent(buttonsPanel, BorderLayout.Location.TOP) contentPanel.addComponent(buttonsPanel, BorderLayout.Location.TOP)
GridLayout gridLayout = new GridLayout(7) GridLayout gridLayout = new GridLayout(8)
buttonsPanel.setLayoutManager(gridLayout) buttonsPanel.setLayoutManager(gridLayout)
searchTextBox = new TextBox(new TerminalSize(40, 1)) searchTextBox = new TextBox(new TerminalSize(40, 1))
@@ -83,6 +87,7 @@ class MainWindowView extends BasicWindow {
Button uploadsButton = new Button("Uploads", {upload()}) Button uploadsButton = new Button("Uploads", {upload()})
Button filesButton = new Button("Files", { files() }) Button filesButton = new Button("Files", { files() })
Button trustButton = new Button("Trust", {trust()}) Button trustButton = new Button("Trust", {trust()})
Button chatButton = new Button("Chat", {chat()})
Button quitButton = new Button("Quit", {close()}) Button quitButton = new Button("Quit", {close()})
LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER) LayoutData layoutData = GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER)
@@ -94,6 +99,7 @@ class MainWindowView extends BasicWindow {
addComponent(uploadsButton, layoutData) addComponent(uploadsButton, layoutData)
addComponent(filesButton, layoutData) addComponent(filesButton, layoutData)
addComponent(trustButton, layoutData) addComponent(trustButton, layoutData)
addComponent(chatButton, layoutData)
addComponent(quitButton, layoutData) addComponent(quitButton, layoutData)
} }
@@ -271,6 +277,10 @@ class MainWindowView extends BasicWindow {
textGUI.addWindowAndWait(new TrustView(trustModel, textGUI, core, sizeForTables())) textGUI.addWindowAndWait(new TrustView(trustModel, textGUI, core, sizeForTables()))
} }
private void chat() {
textGUI.addWindowAndWait(new ChatConsoleView(core, chatModel, textGUI, sizeForTables()))
}
private void refreshStats() { private void refreshStats() {
int inCon = 0 int inCon = 0
int outCon = 0 int outCon = 0

View File

@@ -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()
}

View File

@@ -16,6 +16,26 @@ class ResultsModel {
ResultsModel(UIResultBatchEvent results) { ResultsModel(UIResultBatchEvent results) {
this.results = results this.results = results
model = new TableModel("Name","Size","Hash","Sources","Comment","Certificates") model = new TableModel("Name","Size","Hash","Sources","Comment","Certificates")
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 { results.results.each {
String size = DataHelper.formatSize2Decimal(it.size, false) + "B" String size = DataHelper.formatSize2Decimal(it.size, false) + "B"
String infoHash = Base64.encode(it.infohash.getRoot()) String infoHash = Base64.encode(it.infohash.getRoot())

View File

@@ -44,8 +44,13 @@ class ResultsView extends BasicWindow {
table.setVisibleRows(terminalSize.getRows()) table.setVisibleRows(terminalSize.getRows())
contentPanel.addComponent(table, GridLayout.createLayoutData(Alignment.CENTER, Alignment.CENTER, true, false)) 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()}) 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) setComponent(contentPanel)
closeButton.takeFocus() closeButton.takeFocus()
@@ -109,4 +114,11 @@ class ResultsView extends BasicWindow {
ViewCertificatesView view = new ViewCertificatesView(model, textGUI, core, terminalSize) ViewCertificatesView view = new ViewCertificatesView(model, textGUI, core, terminalSize)
textGUI.addWindowAndWait(view) textGUI.addWindowAndWait(view)
} }
private void sort() {
SortPrompt prompt = new SortPrompt(textGUI)
SortType type = prompt.prompt()
if (type != null)
model.sort(type)
}
} }

View File

@@ -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
}
}

View File

@@ -0,0 +1,5 @@
package com.muwire.clilanterna;
public enum SortType {
NAME_ASC,NAME_DESC,SIZE_ASC,SIZE_DESC
}

View File

@@ -436,7 +436,7 @@ public class Core {
} }
} }
Core core = new Core(props, home, "0.6.4") Core core = new Core(props, home, "0.6.6")
core.startServices() core.startServices()
// ... at the end, sleep or execute script // ... at the end, sleep or execute script

View File

@@ -1,20 +1,24 @@
package com.muwire.core.chat; package com.muwire.core.chat;
enum ChatAction { enum ChatAction {
JOIN(true, false, true), JOIN(true, false, true, false),
LEAVE(false, false, true), LEAVE(false, false, true, false),
SAY(false, false, true), SAY(false, false, true, false),
LIST(true, true, true), LIST(true, true, true, false),
HELP(true, true, true), HELP(true, true, true, false),
INFO(true, true, true), INFO(true, true, true, false),
JOINED(true, true, false); JOINED(true, true, false, false),
TRUST(true, false, true, true),
DISTRUST(true, false, true, true);
final boolean console; final boolean console;
final boolean stateless; final boolean stateless;
final boolean user; final boolean user;
ChatAction(boolean console, boolean stateless, boolean user) { final boolean local;
ChatAction(boolean console, boolean stateless, boolean user, boolean local) {
this.console = console; this.console = console;
this.stateless = stateless; this.stateless = stateless;
this.user = user; this.user = user;
this.local = local;
} }
} }

View File

@@ -1,5 +1,6 @@
package com.muwire.core.chat package com.muwire.core.chat
import java.lang.System.Logger.Level
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
import java.util.concurrent.Executor import java.util.concurrent.Executor
import java.util.concurrent.Executors import java.util.concurrent.Executors
@@ -28,10 +29,10 @@ class ChatClient implements Closeable {
private final TrustService trustService private final TrustService trustService
private final MuWireSettings settings private final MuWireSettings settings
private volatile ChatConnection connection private ChatConnection connection
private volatile boolean connectInProgress private boolean connectInProgress
private volatile long lastRejectionTime private long lastRejectionTime
private volatile Thread connectThread private Thread connectThread
ChatClient(I2PConnector connector, EventBus eventBus, Persona host, Persona me, TrustService trustService, ChatClient(I2PConnector connector, EventBus eventBus, Persona host, Persona me, TrustService trustService,
MuWireSettings settings) { MuWireSettings settings) {
@@ -43,15 +44,19 @@ class ChatClient implements Closeable {
this.settings = settings this.settings = settings
} }
void connectIfNeeded() { synchronized void connectIfNeeded() {
if (connection != null || connectInProgress || (System.currentTimeMillis() - lastRejectionTime < REJECTION_BACKOFF)) if (connection != null || connectInProgress || (System.currentTimeMillis() - lastRejectionTime < REJECTION_BACKOFF))
return return
connectInProgress = true
CONNECTOR.execute({connect()}) CONNECTOR.execute({connect()})
} }
private void connect() { private void connect() {
connectInProgress = true synchronized(this) {
if (!connectInProgress)
return
connectThread = Thread.currentThread() connectThread = Thread.currentThread()
}
Endpoint endpoint = null Endpoint endpoint = null
try { try {
eventBus.publish(new ChatConnectionEvent(status : ChatConnectionAttemptStatus.CONNECTING, persona : host)) eventBus.publish(new ChatConnectionEvent(status : ChatConnectionAttemptStatus.CONNECTING, persona : host))
@@ -72,8 +77,11 @@ class ChatClient implements Closeable {
if (code == 429) { if (code == 429) {
eventBus.publish(new ChatConnectionEvent(status : ChatConnectionAttemptStatus.REJECTED, persona : host)) eventBus.publish(new ChatConnectionEvent(status : ChatConnectionAttemptStatus.REJECTED, persona : host))
try { dos.close() } catch (IOException ignore) {}
endpoint.close() endpoint.close()
synchronized(this) {
lastRejectionTime = System.currentTimeMillis() lastRejectionTime = System.currentTimeMillis()
}
return return
} }
@@ -88,32 +96,47 @@ class ChatClient implements Closeable {
if (version != Constants.CHAT_VERSION) if (version != Constants.CHAT_VERSION)
throw new Exception("Unknown chat version $version") throw new Exception("Unknown chat version $version")
synchronized(this) {
if (!connectInProgress)
return
connection = new ChatConnection(eventBus, endpoint, host, false, trustService, settings) connection = new ChatConnection(eventBus, endpoint, host, false, trustService, settings)
connection.start() connection.start()
}
eventBus.publish(new ChatConnectionEvent(status : ChatConnectionAttemptStatus.SUCCESSFUL, persona : host, eventBus.publish(new ChatConnectionEvent(status : ChatConnectionAttemptStatus.SUCCESSFUL, persona : host,
connection : connection)) connection : connection))
} catch (Exception e) { } catch (Exception e) {
log.log(java.util.logging.Level.WARNING, "connect failed", e)
eventBus.publish(new ChatConnectionEvent(status : ChatConnectionAttemptStatus.FAILED, persona : host)) eventBus.publish(new ChatConnectionEvent(status : ChatConnectionAttemptStatus.FAILED, persona : host))
endpoint?.close() if (endpoint != null) {
try {endpoint.getOutputStream().close() } catch (IOException ignore) {}
endpoint.close()
}
} finally { } finally {
synchronized(this) {
connectInProgress = false connectInProgress = false
connectThread = null connectThread = null
} }
} }
}
void disconnected() { synchronized void disconnected() {
connectInProgress = false connectInProgress = false
connection = null connection = null
} }
@Override @Override
public void close() { synchronized public void close() {
connectInProgress = false
connectThread?.interrupt() connectThread?.interrupt()
connection?.close() connection?.close()
eventBus.publish(new ChatConnectionEvent(status : ChatConnectionAttemptStatus.DISCONNECTED, persona : host)) eventBus.publish(new ChatConnectionEvent(status : ChatConnectionAttemptStatus.DISCONNECTED, persona : host))
} }
void ping() { synchronized void ping() {
connection?.sendPing() connection?.sendPing()
} }
synchronized void sendChat(ChatMessageEvent e) {
connection?.sendChat(e)
}
} }

View File

@@ -109,6 +109,7 @@ class ChatConnection implements ChatLink {
} catch (Exception e) { } catch (Exception e) {
log.log(Level.WARNING,"unhandled exception in reader", e) log.log(Level.WARNING,"unhandled exception in reader", e)
} finally { } finally {
try {endpoint.getOutputStream().close()} catch (IOException ignore) {}
close() close()
} }
} }
@@ -123,6 +124,7 @@ class ChatConnection implements ChatLink {
} catch (Exception e) { } catch (Exception e) {
log.log(Level.WARNING,"unhandled exception in writer",e) log.log(Level.WARNING,"unhandled exception in writer",e)
} finally { } finally {
try {endpoint.getOutputStream().close()} catch (IOException ignore) {}
close() close()
} }
} }

View File

@@ -7,4 +7,8 @@ class ChatConnectionEvent extends Event {
ChatConnectionAttemptStatus status ChatConnectionAttemptStatus status
Persona persona Persona persona
ChatLink connection ChatLink connection
public String toString() {
super.toString() + " " + persona.getHumanReadableName() + " " + status.toString()
}
} }

View File

@@ -51,7 +51,7 @@ class ChatManager {
return return
if (e.sender != me) if (e.sender != me)
return return
clients[e.host]?.connection?.sendChat(e) clients[e.host]?.sendChat(e)
} }
void onChatDisconnectionEvent(ChatDisconnectionEvent e) { void onChatDisconnectionEvent(ChatDisconnectionEvent e) {

View File

@@ -34,6 +34,7 @@ class ChatServer {
private final Map<Destination, ChatLink> connections = new ConcurrentHashMap() private final Map<Destination, ChatLink> connections = new ConcurrentHashMap()
private final Map<String, Set<Persona>> rooms = new ConcurrentHashMap<>() private final Map<String, Set<Persona>> rooms = new ConcurrentHashMap<>()
private final Map<Persona, Set<String>> memberships = 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() private final AtomicBoolean running = new AtomicBoolean()
@@ -49,9 +50,11 @@ class ChatServer {
} }
public void start() { public void start() {
running.set(true) if (!running.compareAndSet(false, true))
return
connections.put(me.destination, LocalChatLink.INSTANCE) connections.put(me.destination, LocalChatLink.INSTANCE)
joinRoom(me, CONSOLE) joinRoom(me, CONSOLE)
shortNames.put(me.getHumanReadableName(), me)
echo("/SAY Welcome to my chat server! Type /HELP for list of available commands.",me.destination) echo("/SAY Welcome to my chat server! Type /HELP for list of available commands.",me.destination)
} }
@@ -105,6 +108,7 @@ class ChatServer {
ChatConnection connection = new ChatConnection(eventBus, endpoint, client, true, trustService, settings) ChatConnection connection = new ChatConnection(eventBus, endpoint, client, true, trustService, settings)
connections.put(endpoint.destination, connection) connections.put(endpoint.destination, connection)
joinRoom(client, CONSOLE) joinRoom(client, CONSOLE)
shortNames.put(client.getHumanReadableName(), client)
connection.start() connection.start()
echo("/SAY Welcome to my chat server! Type /HELP for help on available commands",connection.endpoint.destination) echo("/SAY Welcome to my chat server! Type /HELP for help on available commands",connection.endpoint.destination)
} }
@@ -120,6 +124,7 @@ class ChatServer {
leaveRoom(e.persona, it) leaveRoom(e.persona, it)
} }
} }
shortNames.remove(e.persona.getHumanReadableName())
connections.each { k, v -> connections.each { k, v ->
v.sendLeave(e.persona) v.sendLeave(e.persona)
} }
@@ -188,6 +193,9 @@ class ChatServer {
!command.action.user) !command.action.user)
return return
if (command.action.local && e.sender != me)
return
switch(command.action) { switch(command.action) {
case ChatAction.JOIN : processJoin(command.payload, e); break case ChatAction.JOIN : processJoin(command.payload, e); break
case ChatAction.LEAVE : processLeave(e); break case ChatAction.LEAVE : processLeave(e); break
@@ -195,6 +203,8 @@ class ChatServer {
case ChatAction.LIST : processList(e.sender.destination); break case ChatAction.LIST : processList(e.sender.destination); break
case ChatAction.INFO : processInfo(e.sender.destination); break case ChatAction.INFO : processInfo(e.sender.destination); break
case ChatAction.HELP : processHelp(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
} }
} }
@@ -264,12 +274,14 @@ class ChatServer {
private void processHelp(Destination d) { private void processHelp(Destination d) {
String help = """/SAY String help = """/SAY
Available commands: /JOIN /LEAVE /SAY /LIST /INFO /HELP 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 /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 /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 /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 /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 /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 /HELP - prints this help message
""" """
echo(help, d) echo(help, d)
@@ -292,6 +304,13 @@ class ChatServer {
connections[d]?.sendChat(echo) 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() { void stop() {
if (running.compareAndSet(true, false)) { if (running.compareAndSet(true, false)) {
connections.each { k, v -> connections.each { k, v ->

View File

@@ -7,7 +7,7 @@ class FileTree {
private final TreeNode root = new TreeNode() private final TreeNode root = new TreeNode()
private final Map<File, TreeNode> fileToNode = new ConcurrentHashMap<>() private final Map<File, TreeNode> fileToNode = new ConcurrentHashMap<>()
void add(File file) { synchronized void add(File file) {
List<File> path = new ArrayList<>() List<File> path = new ArrayList<>()
path.add(file) path.add(file)
while (file.getParentFile() != null) { while (file.getParentFile() != null) {
@@ -31,7 +31,7 @@ class FileTree {
} }
} }
boolean remove(File file) { synchronized boolean remove(File file) {
TreeNode node = fileToNode.remove(file) TreeNode node = fileToNode.remove(file)
if (node == null) { if (node == null) {
return false return false

View File

@@ -1,5 +1,5 @@
group = com.muwire group = com.muwire
version = 0.6.4 version = 0.6.6
i2pVersion = 0.9.43 i2pVersion = 0.9.43
groovyVersion = 2.4.15 groovyVersion = 2.4.15
slf4jVersion = 1.7.25 slf4jVersion = 1.7.25

View File

@@ -64,8 +64,11 @@ class BrowseController {
def selectedResults = view.selectedResults() def selectedResults = view.selectedResults()
if (selectedResults == null || selectedResults.isEmpty()) if (selectedResults == null || selectedResults.isEmpty())
return return
def group = application.mvcGroupManager.getGroups()['MainFrame']
selectedResults.removeAll { selectedResults.removeAll {
!mvcGroup.parentGroup.parentGroup.model.canDownload(it.infohash) !group.model.canDownload(it.infohash)
} }
selectedResults.each { result -> selectedResults.each { result ->
@@ -74,11 +77,11 @@ class BrowseController {
result : [result], result : [result],
sources : [model.host.destination], sources : [model.host.destination],
target : file, 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() dismiss()
} }

View File

@@ -71,6 +71,7 @@ class ChatRoomController {
params['console'] = false params['console'] = false
params['host'] = model.host params['host'] = model.host
params['roomTabName'] = newRoom params['roomTabName'] = newRoom
params['chatNotificator'] = view.chatNotificator
mvcGroup.parentGroup.createMVCGroup("chat-room", model.host.getHumanReadableName()+"-"+newRoom, params) mvcGroup.parentGroup.createMVCGroup("chat-room", model.host.getHumanReadableName()+"-"+newRoom, params)
} }
@@ -110,6 +111,7 @@ class ChatRoomController {
params['privateChat'] = true params['privateChat'] = true
params['host'] = model.host params['host'] = model.host
params['roomTabName'] = p.getHumanReadableName() params['roomTabName'] = p.getHumanReadableName()
params['chatNotificator'] = view.chatNotificator
mvcGroup.parentGroup.createMVCGroup("chat-room", groupId, params) mvcGroup.parentGroup.createMVCGroup("chat-room", groupId, params)
} }
@@ -141,6 +143,17 @@ class ChatRoomController {
view.refreshMembersTable() 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() { void leaveRoom() {
if (leftRoom) if (leftRoom)
return return
@@ -180,6 +193,8 @@ class ChatRoomController {
runInsideUIAsync { runInsideUIAsync {
view.roomTextArea.append(toDisplay) view.roomTextArea.append(toDisplay)
trimLines() trimLines()
if (!model.console)
view.chatNotificator.onMessage(mvcGroup.mvcId)
} }
} }
@@ -233,4 +248,27 @@ class ChatRoomController {
view.roomTextArea.replaceRange(null, line0Start, line0End) 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)
}
} }

View File

@@ -15,6 +15,14 @@ class ChatServerController {
@ControllerAction @ControllerAction
void disconnect() { void disconnect() {
switch(model.buttonText) {
case "Disconnect" :
model.buttonText = "Connect"
model.core.eventBus.publish(new UIDisconnectChatEvent(host : model.host)) model.core.eventBus.publish(new UIDisconnectChatEvent(host : model.host))
break
case "Connect" :
model.connect()
break
}
} }
} }

View File

@@ -458,6 +458,7 @@ class MainFrameController {
def params = [:] def params = [:]
params['core'] = model.core params['core'] = model.core
params['host'] = model.core.me params['host'] = model.core.me
params['chatNotificator'] = view.chatNotificator
mvcGroup.createMVCGroup("chat-server","local-chat-server", params) mvcGroup.createMVCGroup("chat-server","local-chat-server", params)
} }
} }
@@ -489,8 +490,10 @@ class MainFrameController {
def params = [:] def params = [:]
params['core'] = model.core params['core'] = model.core
params['host'] = p params['host'] = p
params['chatNotificator'] = view.chatNotificator
mvcGroup.createMVCGroup("chat-server", p.getHumanReadableName(), params) mvcGroup.createMVCGroup("chat-server", p.getHumanReadableName(), params)
} } else
mvcGroup.getChildrenGroups().get(p.getHumanReadableName()).model.connect()
} }
void saveMuWireSettings() { void saveMuWireSettings() {

View File

@@ -2,6 +2,8 @@ package com.muwire.gui
import java.util.logging.Level import java.util.logging.Level
import javax.annotation.Nonnull
import com.muwire.core.Core import com.muwire.core.Core
import com.muwire.core.Persona import com.muwire.core.Persona
import com.muwire.core.chat.ChatCommand import com.muwire.core.chat.ChatCommand
@@ -13,6 +15,7 @@ import com.muwire.core.chat.ChatMessageEvent
import com.muwire.core.chat.UIConnectChatEvent import com.muwire.core.chat.UIConnectChatEvent
import griffon.core.artifact.GriffonModel import griffon.core.artifact.GriffonModel
import griffon.inject.MVCMember
import griffon.transform.Observable import griffon.transform.Observable
import groovy.util.logging.Log import groovy.util.logging.Log
import griffon.metadata.ArtifactProviderFor import griffon.metadata.ArtifactProviderFor
@@ -20,11 +23,16 @@ import griffon.metadata.ArtifactProviderFor
@Log @Log
@ArtifactProviderFor(GriffonModel) @ArtifactProviderFor(GriffonModel)
class ChatServerModel { class ChatServerModel {
@MVCMember @Nonnull
ChatServerView view
Persona host Persona host
Core core Core core
@Observable boolean disconnectActionEnabled @Observable boolean disconnectActionEnabled
@Observable String buttonText = "Disconnect"
@Observable ChatConnectionAttemptStatus status @Observable ChatConnectionAttemptStatus status
@Observable boolean sayActionEnabled
volatile ChatLink link volatile ChatLink link
volatile Thread poller volatile Thread poller
@@ -32,40 +40,65 @@ class ChatServerModel {
void mvcGroupInit(Map<String, String> params) { void mvcGroupInit(Map<String, String> params) {
disconnectActionEnabled = host != core.me // can't disconnect from myself disconnectActionEnabled = host != core.me // can't disconnect from myself
core.eventBus.register(ChatConnectionEvent.class, this)
core.eventBus.with { connect()
register(ChatConnectionEvent.class, this)
publish(new UIConnectChatEvent(host : host))
} }
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 running = true
poller = new Thread({eventLoop()} as Runnable) poller = new Thread({eventLoop()} as Runnable)
poller.setDaemon(true) poller.setDaemon(true)
poller.start() poller.start()
} }
void mvcGroupDestroy() { private void stopPoller() {
running = false running = false
poller?.interrupt() poller?.interrupt()
link = null
} }
void onChatConnectionEvent(ChatConnectionEvent e) { void onChatConnectionEvent(ChatConnectionEvent e) {
if (e.persona == host) { if (e.persona != host)
return
runInsideUIAsync { runInsideUIAsync {
status = e.status status = e.status
} sayActionEnabled = status == ChatConnectionAttemptStatus.SUCCESSFUL
} }
if (e.status == ChatConnectionAttemptStatus.SUCCESSFUL) {
ChatLink link = e.connection ChatLink link = e.connection
if (link == null) if (link == null)
return return
if (link.getPersona() == host) this.link = e.connection
this.link = link
else if (link.getPersona() == null && host == core.me) startPoller()
this.link = link
mvcGroup.childrenGroups.each {k,v ->
v.controller.rejoinRoom()
}
} else {
stopPoller()
}
} }
private void eventLoop() { private void eventLoop() {
Thread.sleep(1000)
while(running) { while(running) {
ChatLink link = this.link ChatLink link = this.link
if (link == null || !link.isUp()) { if (link == null || !link.isUp()) {
@@ -106,6 +139,7 @@ class ChatServerModel {
params['privateChat'] = true params['privateChat'] = true
params['host'] = host params['host'] = host
params['roomTabName'] = e.sender.getHumanReadableName() params['roomTabName'] = e.sender.getHumanReadableName()
params['chatNotificator'] = view.chatNotificator
mvcGroup.createMVCGroup("chat-room",groupId, params) mvcGroup.createMVCGroup("chat-room",groupId, params)
} }

View File

@@ -106,6 +106,8 @@ class MainFrameModel {
@Observable boolean subscribeButtonEnabled @Observable boolean subscribeButtonEnabled
@Observable boolean markNeutralFromTrustedButtonEnabled @Observable boolean markNeutralFromTrustedButtonEnabled
@Observable boolean markDistrustedButtonEnabled @Observable boolean markDistrustedButtonEnabled
@Observable boolean browseFromTrustedButtonEnabled
@Observable boolean chatFromTrustedButtonEnabled
@Observable boolean markNeutralFromDistrustedButtonEnabled @Observable boolean markNeutralFromDistrustedButtonEnabled
@Observable boolean markTrustedButtonEnabled @Observable boolean markTrustedButtonEnabled
@Observable boolean reviewButtonEnabled @Observable boolean reviewButtonEnabled

View File

@@ -39,6 +39,8 @@ class BrowseView {
def p def p
def resultsTable def resultsTable
def lastSortEvent def lastSortEvent
def sequentialDownloadCheckbox
void initUI() { void initUI() {
int rowHeight = application.context.get("row-height") int rowHeight = application.context.get("row-height")
mainFrame = application.windowManager.findWindow("main-frame") mainFrame = application.windowManager.findWindow("main-frame")
@@ -67,6 +69,8 @@ class BrowseView {
button(text : "View Comment", enabled : bind{model.viewCommentActionEnabled}, viewCommentAction) button(text : "View Comment", enabled : bind{model.viewCommentActionEnabled}, viewCommentAction)
button(text : "View Certificates", enabled : bind{model.viewCertificatesActionEnabled}, viewCertificatesAction) button(text : "View Certificates", enabled : bind{model.viewCertificatesActionEnabled}, viewCertificatesAction)
button(text : "Dismiss", dismissAction) button(text : "Dismiss", dismissAction)
label(text : "Download sequentially")
sequentialDownloadCheckbox = checkBox()
} }
} }
@@ -106,8 +110,9 @@ class BrowseView {
else else
model.viewCommentActionEnabled = false model.viewCommentActionEnabled = false
def mainFrameGroup = application.mvcGroupManager.getGroups()['MainFrame']
rows.each { rows.each {
downloadActionEnabled &= mvcGroup.parentGroup.parentGroup.model.canDownload(model.results[it].infohash) downloadActionEnabled &= mainFrameGroup.model.canDownload(model.results[it].infohash)
} }
model.downloadActionEnabled = downloadActionEnabled model.downloadActionEnabled = downloadActionEnabled

View File

@@ -12,6 +12,7 @@ import javax.swing.SwingConstants
import javax.swing.SpringLayout.Constraints import javax.swing.SpringLayout.Constraints
import com.muwire.core.Persona import com.muwire.core.Persona
import com.muwire.core.chat.ChatConnectionAttemptStatus
import java.awt.BorderLayout import java.awt.BorderLayout
import java.awt.event.MouseAdapter import java.awt.event.MouseAdapter
@@ -28,29 +29,33 @@ class ChatRoomView {
@MVCMember @Nonnull @MVCMember @Nonnull
ChatRoomController controller ChatRoomController controller
ChatNotificator chatNotificator
def pane def pane
def parent def parent
def sayField def sayField
def roomTextArea def roomTextArea
def textScrollPane
def membersTable def membersTable
def lastMembersTableSortEvent def lastMembersTableSortEvent
void initUI() { void initUI() {
int rowHeight = application.context.get("row-height") int rowHeight = application.context.get("row-height")
def parentModel = mvcGroup.parentGroup.model
if (model.console || model.privateChat) { if (model.console || model.privateChat) {
pane = builder.panel { pane = builder.panel {
borderLayout() borderLayout()
panel(constraints : BorderLayout.CENTER) { panel(constraints : BorderLayout.CENTER) {
gridLayout(rows : 1, cols : 1) gridLayout(rows : 1, cols : 1)
scrollPane { textScrollPane = scrollPane {
roomTextArea = textArea(editable : false, lineWrap : true, wrapStyleWord : true) roomTextArea = textArea(editable : false, lineWrap : true, wrapStyleWord : true)
} }
} }
panel(constraints : BorderLayout.SOUTH) { panel(constraints : BorderLayout.SOUTH) {
borderLayout() borderLayout()
label(text : "Say something here: ", constraints : BorderLayout.WEST) label(text : "Say something here: ", constraints : BorderLayout.WEST)
sayField = textField(actionPerformed : {controller.say()}, constraints : BorderLayout.CENTER) sayField = textField(enabled : bind {parentModel.sayActionEnabled}, actionPerformed : {controller.say()}, constraints : BorderLayout.CENTER)
button(text : "Say", constraints : BorderLayout.EAST, sayAction) button(enabled : bind {parentModel.sayActionEnabled},text : "Say", constraints : BorderLayout.EAST, sayAction)
} }
} }
} else { } else {
@@ -72,7 +77,7 @@ class ChatRoomView {
} }
panel { panel {
gridLayout(rows : 1, cols : 1) gridLayout(rows : 1, cols : 1)
scrollPane { textScrollPane = scrollPane {
roomTextArea = textArea(editable : false, lineWrap : true, wrapStyleWord : true) roomTextArea = textArea(editable : false, lineWrap : true, wrapStyleWord : true)
} }
} }
@@ -81,12 +86,15 @@ class ChatRoomView {
panel(constraints : BorderLayout.SOUTH) { panel(constraints : BorderLayout.SOUTH) {
borderLayout() borderLayout()
label(text : "Say something here: ", constraints : BorderLayout.WEST) label(text : "Say something here: ", constraints : BorderLayout.WEST)
sayField = textField(actionPerformed : {controller.say()}, constraints : BorderLayout.CENTER) sayField = textField(enabled : bind {parentModel.sayActionEnabled}, actionPerformed : {controller.say()}, constraints : BorderLayout.CENTER)
button(text : "Say", constraints : BorderLayout.EAST, sayAction) 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) { void mvcGroupInit(Map<String,String> args) {
@@ -134,6 +142,9 @@ class ChatRoomView {
JMenuItem privateChat = new JMenuItem("Start Private Chat") JMenuItem privateChat = new JMenuItem("Start Private Chat")
privateChat.addActionListener({controller.privateMessage()}) privateChat.addActionListener({controller.privateMessage()})
menu.add(privateChat) menu.add(privateChat)
JMenuItem browse = new JMenuItem("Browse")
browse.addActionListener({controller.browse()})
menu.add(browse)
JMenuItem markTrusted = new JMenuItem("Mark Trusted") JMenuItem markTrusted = new JMenuItem("Mark Trusted")
markTrusted.addActionListener({controller.markTrusted()}) markTrusted.addActionListener({controller.markTrusted()})
menu.add(markTrusted) menu.add(markTrusted)
@@ -165,6 +176,7 @@ class ChatRoomView {
int index = parent.indexOfComponent(pane) int index = parent.indexOfComponent(pane)
parent.removeTabAt(index) parent.removeTabAt(index)
controller.leaveRoom() controller.leaveRoom()
chatNotificator.roomClosed(mvcGroup.mvcId)
mvcGroup.destroy() mvcGroup.destroy()
} }
} }

View File

@@ -20,18 +20,21 @@ class ChatServerView {
@MVCMember @Nonnull @MVCMember @Nonnull
ChatServerController controller ChatServerController controller
ChatNotificator chatNotificator
def pane def pane
def parent def parent
def childPane
void initUI() { void initUI() {
pane = builder.panel { pane = builder.panel {
borderLayout() borderLayout()
tabbedPane(id : model.host.getHumanReadableName()+"-chat-rooms", constraints : BorderLayout.CENTER) childPane = tabbedPane(id : model.host.getHumanReadableName()+"-chat-rooms", constraints : BorderLayout.CENTER)
panel(constraints : BorderLayout.SOUTH) { panel(constraints : BorderLayout.SOUTH) {
gridLayout(rows : 1, cols : 3) gridLayout(rows : 1, cols : 3)
panel {} panel {}
panel { panel {
button(text : "Disconnect", enabled : bind {model.disconnectActionEnabled}, disconnectAction) button(text : bind {model.buttonText}, enabled : bind {model.disconnectActionEnabled}, disconnectAction)
} }
panel { panel {
label(text : "Connection Status ") label(text : "Connection Status ")
@@ -39,6 +42,9 @@ class ChatServerView {
} }
} }
} }
pane.putClientProperty("mvcId",mvcGroup.mvcId)
pane.putClientProperty("childPane", childPane)
childPane.addChangeListener({e -> chatNotificator.roomTabChanged(e.getSource())})
} }
void mvcGroupInit(Map<String,String> args) { void mvcGroupInit(Map<String,String> args) {
@@ -69,10 +75,12 @@ class ChatServerView {
params['roomTabName'] = 'Console' params['roomTabName'] = 'Console'
params['console'] = true params['console'] = true
params['host'] = model.host params['host'] = model.host
params['chatNotificator'] = chatNotificator
mvcGroup.createMVCGroup("chat-room",model.host.getHumanReadableName()+"-"+ChatServer.CONSOLE, params) mvcGroup.createMVCGroup("chat-room",model.host.getHumanReadableName()+"-"+ChatServer.CONSOLE, params)
} }
def closeTab = { def closeTab = {
if (model.buttonText == "Disconnect")
controller.disconnect() controller.disconnect()
int index = parent.indexOfComponent(pane) int index = parent.indexOfComponent(pane)
parent.removeTabAt(index) parent.removeTabAt(index)

View File

@@ -78,8 +78,10 @@ class MainFrameView {
UISettings settings UISettings settings
ChatNotificator chatNotificator
void initUI() { void initUI() {
chatNotificator = new ChatNotificator(application.getMvcGroupManager())
settings = application.context.get("ui-settings") settings = application.context.get("ui-settings")
int rowHeight = application.context.get("row-height") int rowHeight = application.context.get("row-height")
builder.with { builder.with {
@@ -434,8 +436,8 @@ class MainFrameView {
button(text : "Subscribe", enabled : bind {model.subscribeButtonEnabled}, constraints : gbc(gridx: 0, gridy : 0), subscribeAction) 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 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 : "Mark Distrusted", enabled : bind {model.markDistrustedButtonEnabled}, constraints : gbc(gridx: 2, gridy:0), markDistrustedAction)
button(text : "Browse", constraints:gbc(gridx:3, gridy:0), browseFromTrustedAction) button(text : "Browse", enabled : bind{model.browseFromTrustedButtonEnabled}, constraints:gbc(gridx:3, gridy:0), browseFromTrustedAction)
button(text : "Chat", constraints : gbc(gridx:4, gridy:0), chatFromTrustedAction) button(text : "Chat", enabled : bind{model.chatFromTrustedButtonEnabled} ,constraints : gbc(gridx:4, gridy:0), chatFromTrustedAction)
} }
} }
panel (border : etchedBorder()){ panel (border : etchedBorder()){
@@ -511,7 +513,9 @@ class MainFrameView {
public boolean importData(TransferHandler.TransferSupport support) { public boolean importData(TransferHandler.TransferSupport support) {
def files = support.getTransferable().getTransferData(DataFlavor.javaFileListFlavor) def files = support.getTransferable().getTransferData(DataFlavor.javaFileListFlavor)
files.each { 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() showUploadsWindow.call()
true true
@@ -520,6 +524,7 @@ class MainFrameView {
mainFrame.addWindowListener(new WindowAdapter(){ mainFrame.addWindowListener(new WindowAdapter(){
public void windowClosing(WindowEvent e) { public void windowClosing(WindowEvent e) {
chatNotificator.mainWindowDeactivated()
if (application.getContext().get("tray-icon")) { if (application.getContext().get("tray-icon")) {
if (settings.closeWarning) { if (settings.closeWarning) {
runInsideUIAsync { runInsideUIAsync {
@@ -533,6 +538,13 @@ class MainFrameView {
} else { } else {
closeApplication() closeApplication()
} }
}
public void windowDeactivated(WindowEvent e) {
chatNotificator.mainWindowDeactivated()
}
public void windowActivated(WindowEvent e) {
if (!model.chatPaneButtonEnabled)
chatNotificator.mainWindowActivated()
}}) }})
// search field // search field
@@ -773,10 +785,14 @@ class MainFrameView {
model.subscribeButtonEnabled = false model.subscribeButtonEnabled = false
model.markDistrustedButtonEnabled = false model.markDistrustedButtonEnabled = false
model.markNeutralFromTrustedButtonEnabled = false model.markNeutralFromTrustedButtonEnabled = false
model.chatFromTrustedButtonEnabled = false
model.browseFromTrustedButtonEnabled = false
} else { } else {
model.subscribeButtonEnabled = true model.subscribeButtonEnabled = true
model.markDistrustedButtonEnabled = true model.markDistrustedButtonEnabled = true
model.markNeutralFromTrustedButtonEnabled = true model.markNeutralFromTrustedButtonEnabled = true
model.chatFromTrustedButtonEnabled = true
model.browseFromTrustedButtonEnabled = true
} }
}) })
@@ -825,6 +841,10 @@ class MainFrameView {
} }
}) })
// chat tabs
def chatTabbedPane = builder.getVariable("chat-tabs")
chatTabbedPane.addChangeListener({e -> chatNotificator.serverTabChanged(e.getSource())})
// show tree by default // show tree by default
showSharedFilesTree.call() showSharedFilesTree.call()
@@ -1027,6 +1047,7 @@ class MainFrameView {
model.monitorPaneButtonEnabled = true model.monitorPaneButtonEnabled = true
model.trustPaneButtonEnabled = true model.trustPaneButtonEnabled = true
model.chatPaneButtonEnabled = true model.chatPaneButtonEnabled = true
chatNotificator.mainWindowDeactivated()
} }
def showDownloadsWindow = { def showDownloadsWindow = {
@@ -1038,6 +1059,7 @@ class MainFrameView {
model.monitorPaneButtonEnabled = true model.monitorPaneButtonEnabled = true
model.trustPaneButtonEnabled = true model.trustPaneButtonEnabled = true
model.chatPaneButtonEnabled = true model.chatPaneButtonEnabled = true
chatNotificator.mainWindowDeactivated()
} }
def showUploadsWindow = { def showUploadsWindow = {
@@ -1049,6 +1071,7 @@ class MainFrameView {
model.monitorPaneButtonEnabled = true model.monitorPaneButtonEnabled = true
model.trustPaneButtonEnabled = true model.trustPaneButtonEnabled = true
model.chatPaneButtonEnabled = true model.chatPaneButtonEnabled = true
chatNotificator.mainWindowDeactivated()
} }
def showMonitorWindow = { def showMonitorWindow = {
@@ -1060,6 +1083,7 @@ class MainFrameView {
model.monitorPaneButtonEnabled = false model.monitorPaneButtonEnabled = false
model.trustPaneButtonEnabled = true model.trustPaneButtonEnabled = true
model.chatPaneButtonEnabled = true model.chatPaneButtonEnabled = true
chatNotificator.mainWindowDeactivated()
} }
def showTrustWindow = { def showTrustWindow = {
@@ -1071,6 +1095,7 @@ class MainFrameView {
model.monitorPaneButtonEnabled = true model.monitorPaneButtonEnabled = true
model.trustPaneButtonEnabled = false model.trustPaneButtonEnabled = false
model.chatPaneButtonEnabled = true model.chatPaneButtonEnabled = true
chatNotificator.mainWindowDeactivated()
} }
def showChatWindow = { def showChatWindow = {
@@ -1082,6 +1107,7 @@ class MainFrameView {
model.monitorPaneButtonEnabled = true model.monitorPaneButtonEnabled = true
model.trustPaneButtonEnabled = true model.trustPaneButtonEnabled = true
model.chatPaneButtonEnabled = false model.chatPaneButtonEnabled = false
chatNotificator.mainWindowActivated()
} }
def showSharedFilesTable = { def showSharedFilesTable = {
@@ -1106,6 +1132,7 @@ class MainFrameView {
if (rv == JFileChooser.APPROVE_OPTION) { if (rv == JFileChooser.APPROVE_OPTION) {
chooser.getSelectedFiles().each { chooser.getSelectedFiles().each {
File canonical = it.getCanonicalFile() File canonical = it.getCanonicalFile()
model.core.fileManager.negativeTree.remove(canonical)
model.core.eventBus.publish(new FileSharedEvent(file : canonical)) model.core.eventBus.publish(new FileSharedEvent(file : canonical))
} }
} }

View 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))
}
}
}

View 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;
}
}