Compare commits

..

27 Commits

Author SHA1 Message Date
Zlatin Balevsky
656b62fc2e 0.0.4 with download retry 2019-06-01 18:31:36 +01:00
Zlatin Balevsky
13b3f0f63b retry implemented 2019-06-01 18:30:30 +01:00
Zlatin Balevsky
98ea8154a5 store done pieces on disk to enable resume 2019-06-01 18:09:14 +01:00
Zlatin Balevsky
82377aa9df hook up cancel button 2019-06-01 17:44:52 +01:00
Zlatin Balevsky
bd2368e23a cancelled downloader state 2019-06-01 17:31:18 +01:00
Zlatin Balevsky
70078c309b add cancel and retry buttons, not hooked up yet 2019-06-01 17:30:29 +01:00
Zlatin Balevsky
15a0eda713 preserve selection in downloads table 2019-06-01 17:09:23 +01:00
Zlatin Balevsky
9645716e18 prevent rare stacktraces on shutdown 2019-06-01 16:55:37 +01:00
Zlatin Balevsky
03d6af39ed icon for closing tabs 2019-06-01 16:43:05 +01:00
Zlatin Balevsky
9435cb003b Show warning if cannot find I2P router 2019-06-01 16:36:23 +01:00
Zlatin Balevsky
63399803d5 ui tweaks 2019-06-01 15:59:55 +01:00
Zlatin Balevsky
4d6541030f disable system l&f on osx 2019-06-01 14:55:17 +01:00
Zlatin Balevsky
16c51e7cd6 add a failed download state 2019-06-01 14:14:20 +01:00
Zlatin Balevsky
9d75550b6f do not show local searches in monitor 2019-06-01 13:48:12 +01:00
Zlatin Balevsky
1996681677 incoming searches monitor 2019-06-01 13:44:46 +01:00
Zlatin Balevsky
9dac1891b2 connection monitor 2019-06-01 13:32:40 +01:00
Zlatin Balevsky
1255ac936b close connections on shutdown 2019-06-01 13:04:22 +01:00
Zlatin Balevsky
2db3276b07 fix rare NPE on shutdown 2019-06-01 13:03:42 +01:00
Zlatin Balevsky
7e3b0795af disable buttons if no row is selected 2019-06-01 12:23:20 +01:00
Zlatin Balevsky
4bb27b84de release 0.0.3 for fixed first hop and tabbed search ui 2019-06-01 11:11:22 +01:00
Zlatin Balevsky
d5b8c0c694 fix parsing of first hop 2019-06-01 11:03:27 +01:00
Zlatin Balevsky
1e314b3cca serialize firstHop 2019-06-01 10:44:24 +01:00
Zlatin Balevsky
6f02e3e9c0 hook up buttons to tabbed results 2019-06-01 10:04:10 +01:00
Zlatin Balevsky
9f5f21376a one tab per search 2019-06-01 09:29:03 +01:00
Zlatin Balevsky
d2231b8e38 attach uuid to search results 2019-06-01 07:20:39 +01:00
Zlatin Balevsky
77fac612cc 0.0.2 for sig type change 2019-05-31 17:39:59 +01:00
Zlatin Balevsky
5875e7d03f change sig type 2019-05-31 17:36:08 +01:00
29 changed files with 536 additions and 65 deletions

View File

@@ -4,7 +4,7 @@ import net.i2p.crypto.SigType
class Constants {
public static final byte PERSONA_VERSION = (byte)1
public static final SigType SIG_TYPE = SigType.ECDSA_SHA512_P521 // TODO: decide which
public static final SigType SIG_TYPE = SigType.EdDSA_SHA512_Ed25519
public static final int MAX_HEADER_SIZE = 0x1 << 14
public static final int MAX_HEADERS = 16

View File

@@ -54,7 +54,8 @@ public class Core {
final EventBus eventBus
final Persona me
final File home
private final TrustService trustService
private final PersisterService persisterService
private final HostCache hostCache
@@ -64,7 +65,8 @@ public class Core {
private final ConnectionEstablisher connectionEstablisher
private final HasherService hasherService
public Core(MuWireSettings props, File home) {
public Core(MuWireSettings props, File home) {
this.home = home
log.info "Initializing I2P context"
I2PAppContext.getGlobalContext().logManager()
I2PAppContext.getGlobalContext()._logManager = new MuWireLogManager()
@@ -164,7 +166,7 @@ public class Core {
eventBus.register(ResultsEvent.class, searchManager)
log.info("initializing download manager")
DownloadManager downloadManager = new DownloadManager(eventBus, i2pConnector)
DownloadManager downloadManager = new DownloadManager(eventBus, i2pConnector, new File(home, "incompletes"))
eventBus.register(UIDownloadEvent.class, downloadManager)
log.info("initializing upload manager")
@@ -196,6 +198,10 @@ public class Core {
connectionEstablisher.start()
hostCache.waitForLoad()
}
public void shutdown() {
connectionManager.shutdown()
}
static main(args) {
def home = System.getProperty("user.home") + File.separator + ".MuWire"

View File

@@ -82,7 +82,6 @@ abstract class Connection implements Closeable {
read()
}
} catch (SocketTimeoutException e) {
close()
} catch (Exception e) {
log.log(Level.WARNING,"unhandled exception in reader",e)
} finally {
@@ -120,6 +119,7 @@ abstract class Connection implements Closeable {
query.type = "Search"
query.version = 1
query.uuid = e.searchEvent.getUuid()
query.firstHop = e.firstHop
// TODO: first hop figure out
query.keywords = e.searchEvent.getSearchTerms()
query.replyTo = e.getReceivedOn().toBase64()
@@ -164,7 +164,7 @@ abstract class Connection implements Closeable {
QueryEvent event = new QueryEvent ( searchEvent : searchEvent,
replyTo : replyTo,
receivedOn : endpoint.destination,
firstHop : Boolean.parseBoolean(search.firstHop) )
firstHop : search.firstHop )
eventBus.publish(event)
}

View File

@@ -204,7 +204,7 @@ class ConnectionAcceptor {
byte [] payload = new byte[jsonSize]
dis.readFully(payload)
def json = slurper.parse(payload)
eventBus.publish(ResultsParser.parse(sender, json))
eventBus.publish(ResultsParser.parse(sender, resultsUUID, json))
}
} catch (IOException | UnexpectedResultsException | InvalidSearchResultException bad) {
log.log(Level.WARNING, "failed to process POST", bad)

View File

@@ -58,6 +58,8 @@ abstract class ConnectionManager {
abstract void onConnectionEvent(ConnectionEvent e)
abstract void onDisconnectionEvent(DisconnectionEvent e)
abstract void shutdown()
protected void sendPings() {
final long now = System.currentTimeMillis()

View File

@@ -71,4 +71,8 @@ class LeafConnectionManager extends ConnectionManager {
log.severe("removed destination not present in connection manager ${e.destination.toBase32()}")
}
@Override
void shutdown() {
}
}

View File

@@ -20,7 +20,7 @@ class UltrapeerConnectionManager extends ConnectionManager {
final Map<Destination, PeerConnection> peerConnections = new ConcurrentHashMap()
final Map<Destination, LeafConnection> leafConnections = new ConcurrentHashMap()
UltrapeerConnectionManager() {}
public UltrapeerConnectionManager(EventBus eventBus, Persona me, int maxPeers, int maxLeafs,
@@ -100,6 +100,14 @@ class UltrapeerConnectionManager extends ConnectionManager {
if (removed == null)
log.severe("Removed connection not present in either leaf or peer map ${e.destination.toBase32()}")
}
@Override
void shutdown() {
peerConnections.each {k,v -> v.close() }
leafConnections.each {k,v -> v.close() }
peerConnections.clear()
leafConnections.clear()
}
void forwardQueryToLeafs(QueryEvent e) {

View File

@@ -0,0 +1,25 @@
package com.muwire.core.download
class BadHashException extends Exception {
public BadHashException() {
super();
}
public BadHashException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
public BadHashException(String message, Throwable cause) {
super(message, cause);
}
public BadHashException(String message) {
super(message);
}
public BadHashException(Throwable cause) {
super(cause);
}
}

View File

@@ -11,10 +11,13 @@ public class DownloadManager {
private final EventBus eventBus
private final I2PConnector connector
private final Executor executor
private final File incompletes
public DownloadManager(EventBus eventBus, I2PConnector connector) {
public DownloadManager(EventBus eventBus, I2PConnector connector, File incompletes) {
this.eventBus = eventBus
this.connector = connector
this.incompletes = incompletes
incompletes.mkdir()
this.executor = Executors.newCachedThreadPool({ r ->
Thread rv = new Thread(r)
rv.setName("download-worker")
@@ -25,9 +28,14 @@ public class DownloadManager {
public void onUIDownloadEvent(UIDownloadEvent e) {
def downloader = new Downloader(e.target, e.result.size,
e.result.infohash, e.result.pieceSize, connector, e.result.sender.destination)
def downloader = new Downloader(this, e.target, e.result.size,
e.result.infohash, e.result.pieceSize, connector, e.result.sender.destination,
incompletes)
executor.execute({downloader.download()} as Runnable)
eventBus.publish(new DownloadStartedEvent(downloader : downloader))
}
void resume(Downloader downloader) {
executor.execute({downloader.download() as Runnable})
}
}

View File

@@ -127,11 +127,8 @@ class DownloadSession {
byte [] hash = digest.digest()
byte [] expected = new byte[32]
System.arraycopy(infoHash.getHashList(), piece * 32, expected, 0, 32)
if (hash != expected) {
log.warning("hash mismatch")
endpoint.close()
return
}
if (hash != expected)
throw new BadHashException()
pieces.markDownloaded(piece)
} finally {

View File

@@ -2,14 +2,20 @@ package com.muwire.core.download
import com.muwire.core.InfoHash
import com.muwire.core.connection.Endpoint
import java.util.logging.Level
import com.muwire.core.Constants
import com.muwire.core.connection.I2PConnector
import groovy.util.logging.Log
import net.i2p.data.Destination
@Log
public class Downloader {
public enum DownloadState { CONNECTING, DOWNLOADING, FINISHED }
public enum DownloadState { CONNECTING, DOWNLOADING, FAILED, CANCELLED, FINISHED }
private final DownloadManager downloadManager
private final File file
private final Pieces pieces
private final long length
@@ -18,17 +24,24 @@ public class Downloader {
private final I2PConnector connector
private final Destination destination
private final int nPieces
private final File piecesFile
private Endpoint endpoint
private volatile DownloadSession currentSession
private volatile DownloadState currentState
private volatile boolean cancelled
private volatile Thread downloadThread
public Downloader(File file, long length, InfoHash infoHash, int pieceSizePow2, I2PConnector connector, Destination destination) {
public Downloader(DownloadManager downloadManager, File file, long length, InfoHash infoHash,
int pieceSizePow2, I2PConnector connector, Destination destination,
File incompletes) {
this.downloadManager = downloadManager
this.file = file
this.infoHash = infoHash
this.length = length
this.connector = connector
this.destination = destination
this.piecesFile = new File(incompletes, file.getName()+".pieces")
this.pieceSize = 1 << pieceSizePow2
int nPieces
@@ -43,14 +56,45 @@ public class Downloader {
}
void download() {
Endpoint endpoint = connector.connect(destination)
currentState = DownloadState.DOWNLOADING
while(!pieces.isComplete()) {
currentSession = new DownloadSession(pieces, infoHash, endpoint, file, pieceSize, length)
currentSession.request()
readPieces()
downloadThread = Thread.currentThread()
Endpoint endpoint = null
try {
endpoint = connector.connect(destination)
currentState = DownloadState.DOWNLOADING
while(!pieces.isComplete()) {
currentSession = new DownloadSession(pieces, infoHash, endpoint, file, pieceSize, length)
currentSession.request()
writePieces()
}
currentState = DownloadState.FINISHED
piecesFile.delete()
} catch (Exception bad) {
log.log(Level.WARNING,"Exception while downloading",bad)
if (cancelled)
currentState = DownloadState.CANCELLED
else if (currentState != DownloadState.FINISHED)
currentState = DownloadState.FAILED
} finally {
endpoint?.close()
}
}
void readPieces() {
if (!piecesFile.exists())
return
piecesFile.withReader {
int piece = Integer.parseInt(it.readLine())
pieces.markDownloaded(piece)
}
}
void writePieces() {
piecesFile.withPrintWriter { writer ->
pieces.getDownloaded().each { piece ->
writer.println(piece)
}
}
currentState = DownloadState.FINISHED
endpoint.close()
}
public long donePieces() {
@@ -66,4 +110,13 @@ public class Downloader {
public DownloadState getCurrentState() {
currentState
}
public void cancel() {
cancelled = true
downloadThread?.interrupt()
}
public void resume() {
downloadManager.resume(this)
}
}

View File

@@ -33,6 +33,14 @@ class Pieces {
}
}
def getDownloaded() {
def rv = []
for (int i = bitSet.nextSetBit(0); i >= 0; i = bitSet.nextSetBit(i+1)) {
rv << i
}
rv
}
synchronized void markDownloaded(int piece) {
bitSet.set(piece)
}

View File

@@ -9,7 +9,7 @@ import com.muwire.core.util.DataUtil
import net.i2p.data.Base64
class ResultsParser {
public static UIResultEvent parse(Persona p, def json) throws InvalidSearchResultException {
public static UIResultEvent parse(Persona p, UUID uuid, def json) throws InvalidSearchResultException {
if (json.type != "Result")
throw new InvalidSearchResultException("not a result json")
if (json.version != 1)
@@ -46,7 +46,8 @@ class ResultsParser {
name : name,
size : size,
infohash : parsedIH,
pieceSize : pieceSize)
pieceSize : pieceSize,
uuid : uuid)
} catch (Exception e) {
throw new InvalidSearchResultException("parsing search result failed",e)
}

View File

@@ -55,7 +55,8 @@ class ResultsSender {
name : it.getFile().getName(),
size : length,
infohash : it.getInfoHash(),
pieceSize : FileHasher.getPieceSize(length)
pieceSize : FileHasher.getPieceSize(length),
uuid : uuid
)
eventBus.publish(uiResultEvent)
}

View File

@@ -6,6 +6,7 @@ import com.muwire.core.Persona
class UIResultEvent extends Event {
Persona sender
UUID uuid
String name
long size
InfoHash infohash

View File

@@ -1,5 +1,5 @@
group = com.muwire
version = 0.0.1
version = 0.0.4
groovyVersion = 2.4.15
slf4jVersion = 1.7.25
spockVersion = 1.1-groovy-2.4

View File

@@ -16,4 +16,9 @@ mvcGroups {
view = 'com.muwire.gui.MainFrameView'
controller = 'com.muwire.gui.MainFrameController'
}
'SearchTab' {
model = 'com.muwire.gui.SearchTabModel'
view = 'com.muwire.gui.SearchTabView'
controller = 'com.muwire.gui.SearchTabController'
}
}

View File

@@ -3,6 +3,8 @@ package com.muwire.gui
import griffon.core.GriffonApplication
import griffon.core.artifact.GriffonController
import griffon.core.controller.ControllerAction
import griffon.core.mvc.MVCGroup
import griffon.core.mvc.MVCGroupConfiguration
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import javax.annotation.Nonnull
@@ -30,20 +32,38 @@ class MainFrameController {
@ControllerAction
void search() {
def search = builder.getVariable("search-field").text
def searchEvent = new SearchEvent(searchTerms : [search], uuid : UUID.randomUUID())
def uuid = UUID.randomUUID()
Map<String, Object> params = new HashMap<>()
params["search-terms"] = search
params["uuid"] = uuid.toString()
def group = mvcGroup.createMVCGroup("SearchTab", uuid.toString(), params)
model.results[uuid.toString()] = group
def searchEvent = new SearchEvent(searchTerms : [search], uuid : uuid)
core.eventBus.publish(new QueryEvent(searchEvent : searchEvent, firstHop : true,
replyTo: core.me.destination, receivedOn: core.me.destination))
}
private def selectedResult() {
def resultsTable = builder.getVariable("results-table")
int row = resultsTable.getSelectedRow()
model.results[row]
def selected = builder.getVariable("result-tabs").getSelectedComponent()
def group = selected.getClientProperty("mvc-group")
def table = selected.getClientProperty("results-table")
int row = table.getSelectedRow()
if (row == -1)
return
group.model.results[row]
}
private def selectedDownload() {
def selected = builder.getVariable("downloads-table").getSelectedRow()
model.downloads[selected].downloader
}
@ControllerAction
void download() {
def result = selectedResult()
if (result == null)
return // TODO disable button
def file = new File(application.context.get("muwire-settings").downloadLocation, result.name)
core.eventBus.publish(new UIDownloadEvent(result : result, target : file))
}
@@ -51,15 +71,31 @@ class MainFrameController {
@ControllerAction
void trust() {
def result = selectedResult()
if (result == null)
return // TODO disable button
core.eventBus.publish( new TrustEvent(destination : result.sender.destination, level : TrustLevel.TRUSTED))
}
@ControllerAction
void distrust() {
def result = selectedResult()
if (result == null)
return // TODO disable button
core.eventBus.publish( new TrustEvent(destination : result.sender.destination, level : TrustLevel.DISTRUSTED))
}
@ControllerAction
void cancel() {
def downloader = selectedDownload()
downloader.cancel()
}
@ControllerAction
void resume() {
def downloader = selectedDownload()
downloader.resume()
}
void mvcGroupInit(Map<String, String> args) {
application.addPropertyChangeListener("core", {e->
core = e.getNewValue()

View File

@@ -0,0 +1,11 @@
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 SearchTabController {
}

View File

@@ -21,7 +21,11 @@ class Initialize extends AbstractLifecycleHandler {
@Override
void execute() {
lookAndFeel((isMacOSX ? 'system' : 'nimbus'), 'gtk', ['metal', [boldFonts: false]])
if (isMacOSX()) {
lookAndFeel('nimbus') // otherwise the file chooser doesn't open???
} else {
lookAndFeel('system', 'gtk')
}
}
}

View File

@@ -16,6 +16,7 @@ import static griffon.util.GriffonApplicationUtils.isMacOSX
import static groovy.swing.SwingBuilder.lookAndFeel
import java.beans.PropertyChangeEvent
import java.util.logging.Level
@Log
class Ready extends AbstractLifecycleHandler {
@@ -81,8 +82,15 @@ class Ready extends AbstractLifecycleHandler {
props.write(it)
}
}
Core core = new Core(props, home)
Core core
try {
core = new Core(props, home)
} catch (Exception bad) {
log.log(Level.SEVERE,"couldn't initialize core",bad)
JOptionPane.showMessageDialog(null, "Couldn't connect to I2P router. Make sure I2P is running and restart MuWire",
"Can't connect to I2P router", JOptionPane.WARNING_MESSAGE)
System.exit(0)
}
core.startServices()
application.context.put("muwire-settings", props)
application.context.put("core",core)

View File

@@ -0,0 +1,25 @@
import javax.annotation.Nonnull
import javax.inject.Inject
import org.codehaus.griffon.runtime.core.AbstractLifecycleHandler
import com.muwire.core.Core
import griffon.core.GriffonApplication
import groovy.util.logging.Log
@Log
class Shutdown extends AbstractLifecycleHandler {
@Inject
Shutdown(@Nonnull GriffonApplication application) {
super(application)
}
@Override
void execute() {
log.info("shutting down")
Core core = application.context.get("core")
core.shutdown()
}
}

View File

@@ -1,5 +1,7 @@
package com.muwire.gui
import java.util.concurrent.ConcurrentHashMap
import javax.annotation.Nonnull
import javax.inject.Inject
import javax.swing.JTable
@@ -13,6 +15,7 @@ import com.muwire.core.download.DownloadStartedEvent
import com.muwire.core.files.FileHashedEvent
import com.muwire.core.files.FileLoadedEvent
import com.muwire.core.files.FileSharedEvent
import com.muwire.core.search.QueryEvent
import com.muwire.core.search.UIResultEvent
import com.muwire.core.trust.TrustEvent
import com.muwire.core.trust.TrustService
@@ -21,9 +24,11 @@ import com.muwire.core.upload.UploadFinishedEvent
import griffon.core.GriffonApplication
import griffon.core.artifact.GriffonModel
import griffon.core.mvc.MVCGroup
import griffon.inject.MVCMember
import griffon.transform.FXObservable
import griffon.transform.Observable
import net.i2p.data.Destination
import griffon.metadata.ArtifactProviderFor
@ArtifactProviderFor(GriffonModel)
@@ -33,12 +38,18 @@ class MainFrameModel {
@Inject @Nonnull GriffonApplication application
@Observable boolean coreInitialized = false
@Observable def results = []
@Observable def downloads = []
@Observable def uploads = []
@Observable def shared = []
def results = new ConcurrentHashMap<>()
def downloads = []
def uploads = []
def shared = []
def connectionList = []
def searches = new LinkedList()
@Observable int connections
@Observable String me
@Observable boolean searchButtonsEnabled
@Observable boolean cancelButtonEnabled
@Observable boolean retryButtonEnabled
private final Set<InfoHash> infoHashes = new HashSet<>()
@@ -58,22 +69,26 @@ class MainFrameModel {
core.eventBus.register(UploadEvent.class, this)
core.eventBus.register(UploadFinishedEvent.class, this)
core.eventBus.register(TrustEvent.class, this)
core.eventBus.register(QueryEvent.class, this)
})
Timer timer = new Timer("download-pumper", true)
timer.schedule({
runInsideUIAsync {
builder.getVariable("downloads-table").model.fireTableDataChanged()
builder.getVariable("uploads-table").model.fireTableDataChanged()
if (!mvcGroup.alive)
return
builder.getVariable("uploads-table")?.model.fireTableDataChanged()
def downloadTable = builder.getVariable("downloads-table")
int selectedRow = downloadTable.getSelectedRow()
downloadTable.model.fireTableDataChanged()
downloadTable.selectionModel.setSelectionInterval(selectedRow,selectedRow)
}
}, 1000, 1000)
}
void onUIResultEvent(UIResultEvent e) {
runInsideUIAsync {
results << e
JTable table = builder.getVariable("results-table")
table.model.fireTableDataChanged()
}
MVCGroup resultsGroup = results.get(e.uuid)
resultsGroup?.model.handleResult(e)
}
void onDownloadStartedEvent(DownloadStartedEvent e) {
@@ -83,14 +98,23 @@ class MainFrameModel {
}
void onConnectionEvent(ConnectionEvent e) {
if (e.getStatus() != ConnectionAttemptStatus.SUCCESSFUL)
return
runInsideUIAsync {
connections = core.connectionManager.getConnections().size()
connectionList.add(e.endpoint.destination)
JTable table = builder.getVariable("connections-table")
table.model.fireTableDataChanged()
}
}
void onDisconnectionEvent(DisconnectionEvent e) {
runInsideUIAsync {
connections = core.connectionManager.getConnections().size()
connectionList.remove(e.destination)
JTable table = builder.getVariable("connections-table")
table.model.fireTableDataChanged()
}
}
@@ -140,4 +164,24 @@ class MainFrameModel {
table.model.fireTableDataChanged()
}
}
void onQueryEvent(QueryEvent e) {
if (e.replyTo == core.me.destination)
return
StringBuilder sb = new StringBuilder()
e.searchEvent.searchTerms?.each {
sb.append(it)
sb.append(" ")
}
def search = sb.toString()
if (search.trim().size() == 0)
return
runInsideUIAsync {
searches.addFirst(search)
while(searches.size() > 200)
searches.removeLast()
JTable table = builder.getVariable("searches-table")
table.model.fireTableDataChanged()
}
}
}

View File

@@ -0,0 +1,42 @@
package com.muwire.gui
import javax.annotation.Nonnull
import javax.inject.Inject
import javax.swing.JTable
import com.muwire.core.Core
import com.muwire.core.search.UIResultEvent
import griffon.core.artifact.GriffonModel
import griffon.core.mvc.MVCGroup
import griffon.inject.MVCMember
import griffon.transform.Observable
import griffon.metadata.ArtifactProviderFor
@ArtifactProviderFor(GriffonModel)
class SearchTabModel {
@MVCMember @Nonnull
FactoryBuilderSupport builder
Core core
String uuid
def results = []
void mvcGroupInit(Map<String, String> args) {
core = mvcGroup.parentGroup.model.core
mvcGroup.parentGroup.model.results[UUID.fromString(uuid)] = mvcGroup
}
void mvcGroupDestroy() {
mvcGroup.parentGroup.model.results.remove(uuid)
}
void handleResult(UIResultEvent e) {
runInsideUIAsync {
results << e
JTable table = builder.getVariable("results-table")
table.model.fireTableDataChanged()
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 298 B

View File

@@ -9,9 +9,11 @@ import javax.swing.Box
import javax.swing.BoxLayout
import javax.swing.JFileChooser
import javax.swing.JSplitPane
import javax.swing.ListSelectionModel
import javax.swing.SwingConstants
import javax.swing.border.Border
import com.muwire.core.download.Downloader
import com.muwire.core.files.FileSharedEvent
import java.awt.BorderLayout
@@ -48,6 +50,7 @@ class MainFrameView {
gridLayout(rows:1, cols: 2)
button(text: "Searches", actionPerformed : showSearchWindow)
button(text: "Uploads", actionPerformed : showUploadsWindow)
button(text: "Monitor", actionPerformed : showMonitorWindow)
}
panel(constraints: BorderLayout.CENTER) {
borderLayout()
@@ -67,22 +70,11 @@ class MainFrameView {
continuousLayout : true, constraints : BorderLayout.CENTER) {
panel (constraints : JSplitPane.TOP) {
borderLayout()
scrollPane (constraints : BorderLayout.CENTER){
table(id : "results-table") {
tableModel(list: model.results) {
closureColumn(header: "Name", type: String, read : {row -> row.name})
closureColumn(header: "Size", preferredWidth: 150, type: Long, read : {row -> row.size})
closureColumn(header: "Sender", type: String, read : {row -> row.sender.getHumanReadableName()})
closureColumn(header: "Trust", type: String, read : {row ->
model.core.trustService.getLevel(row.sender.destination)
})
}
}
}
tabbedPane(id : "result-tabs", constraints: BorderLayout.CENTER)
panel(constraints : BorderLayout.SOUTH) {
button(text : "Download", downloadAction)
button(text : "Trust", trustAction)
button(text : "Distrust", distrustAction)
button(text : "Download", enabled : bind {model.searchButtonsEnabled}, downloadAction)
button(text : "Trust", enabled: bind {model.searchButtonsEnabled }, trustAction)
button(text : "Distrust", enabled : bind {model.searchButtonsEnabled}, distrustAction)
}
}
panel (constraints : JSplitPane.BOTTOM) {
@@ -105,6 +97,10 @@ class MainFrameView {
}
}
}
panel (constraints : BorderLayout.SOUTH) {
button(text: "Cancel", enabled : bind {model.cancelButtonEnabled }, cancelAction )
button(text: "Retry", enabled : bind {model.retryButtonEnabled}, resumeAction)
}
}
}
}
@@ -113,7 +109,7 @@ class MainFrameView {
panel {
borderLayout()
panel (constraints : BorderLayout.NORTH) {
button(text : "Shared files", actionPerformed : shareFiles)
button(text : "Click here to share files", actionPerformed : shareFiles)
}
scrollPane ( constraints : BorderLayout.CENTER) {
table(id : "shared-files-table") {
@@ -126,7 +122,9 @@ class MainFrameView {
}
panel {
borderLayout()
label("Uploads", constraints : BorderLayout.NORTH)
panel (constraints : BorderLayout.NORTH){
label("Uploads")
}
scrollPane (constraints : BorderLayout.CENTER) {
table(id : "uploads-table") {
tableModel(list : model.uploads) {
@@ -143,6 +141,35 @@ class MainFrameView {
}
}
}
panel (constraints: "monitor window") {
gridLayout(rows : 1, cols : 2)
panel {
borderLayout()
panel (constraints : BorderLayout.NORTH){
label("Connections")
}
scrollPane(constraints : BorderLayout.CENTER) {
table(id : "connections-table") {
tableModel(list : model.connectionList) {
closureColumn(header : "Destination", type: String, read : { row -> row.toBase32() })
}
}
}
}
panel {
borderLayout()
panel (constraints : BorderLayout.NORTH){
label("Incoming searches")
}
scrollPane(constraints : BorderLayout.CENTER) {
table(id : "searches-table") {
tableModel(list : model.searches) {
closureColumn(header : "Keywords", type : String, read : { it })
}
}
}
}
}
}
panel (border: etchedBorder(), constraints : BorderLayout.SOUTH) {
borderLayout()
@@ -156,6 +183,30 @@ class MainFrameView {
}
}
}
void mvcGroupInit(Map<String, String> args) {
def downloadsTable = builder.getVariable("downloads-table")
def selectionModel = downloadsTable.getSelectionModel()
selectionModel.setSelectionMode(ListSelectionModel.SINGLE_SELECTION)
selectionModel.addListSelectionListener({
int selectedRow = downloadsTable.getSelectedRow()
def downloader = model.downloads[selectedRow].downloader
switch(downloader.getCurrentState()) {
case Downloader.DownloadState.CONNECTING :
case Downloader.DownloadState.DOWNLOADING :
model.cancelButtonEnabled = true
model.retryButtonEnabled = false
break
case Downloader.DownloadState.FAILED:
model.cancelButtonEnabled = false
model.retryButtonEnabled = true
break
default:
model.cancelButtonEnabled = false
model.retryButtonEnabled = false
}
})
}
def showSearchWindow = {
def cardsPanel = builder.getVariable("cards-panel")
@@ -167,6 +218,11 @@ class MainFrameView {
cardsPanel.getLayout().show(cardsPanel, "uploads window")
}
def showMonitorWindow = {
def cardsPanel = builder.getVariable("cards-panel")
cardsPanel.getLayout().show(cardsPanel,"monitor window")
}
def shareFiles = {
def chooser = new JFileChooser()
chooser.setDialogTitle("Select file or directory to share")

View File

@@ -0,0 +1,80 @@
package com.muwire.gui
import griffon.core.artifact.GriffonView
import griffon.core.mvc.MVCGroup
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import javax.swing.ListSelectionModel
import javax.swing.SwingConstants
import java.awt.BorderLayout
import javax.annotation.Nonnull
@ArtifactProviderFor(GriffonView)
class SearchTabView {
@MVCMember @Nonnull
FactoryBuilderSupport builder
@MVCMember @Nonnull
SearchTabModel model
def pane
def parent
def searchTerms
void initUI() {
builder.with {
def resultsTable
def pane = scrollPane {
resultsTable = table(id : "results-table") {
tableModel(list: model.results) {
closureColumn(header: "Name", type: String, read : {row -> row.name})
closureColumn(header: "Size", preferredWidth: 150, type: Long, read : {row -> row.size})
closureColumn(header: "Sender", type: String, read : {row -> row.sender.getHumanReadableName()})
closureColumn(header: "Trust", type: String, read : {row ->
model.core.trustService.getLevel(row.sender.destination)
})
}
}
}
this.pane = pane
this.pane.putClientProperty("mvc-group", mvcGroup)
this.pane.putClientProperty("results-table",resultsTable)
def selectionModel = resultsTable.getSelectionModel()
selectionModel.setSelectionMode(ListSelectionModel.SINGLE_SELECTION)
selectionModel.addListSelectionListener( {
mvcGroup.parentGroup.model.searchButtonsEnabled = true
})
}
}
void mvcGroupInit(Map<String, String> args) {
searchTerms = args["search-terms"]
parent = mvcGroup.parentGroup.view.builder.getVariable("result-tabs")
parent.addTab(searchTerms, pane)
int index = parent.indexOfTab(searchTerms)
def tabPanel
builder.with {
tabPanel = panel {
borderLayout()
panel {
label(text : searchTerms, constraints : BorderLayout.CENTER)
}
button(icon : imageIcon("/close_tab.png"), preferredSize : [20,20], constraints : BorderLayout.EAST, // TODO: in osx is probably WEST
actionPerformed : closeTab )
}
}
parent.setTabComponentAt(index, tabPanel)
}
def closeTab = {
int index = parent.indexOfTab(searchTerms)
parent.removeTabAt(index)
mvcGroup.parentGroup.model.searchButtonsEnabled = false
mvcGroup.destroy()
}
}

View File

@@ -0,0 +1,25 @@
package com.muwire.gui
import griffon.core.test.GriffonFestRule
import org.fest.swing.fixture.FrameFixture
import org.junit.Rule
import org.junit.Test
import static org.junit.Assert.fail
class SearchTabIntegrationTest {
static {
System.setProperty('griffon.swing.edt.violations.check', 'true')
System.setProperty('griffon.swing.edt.hang.monitor', 'true')
}
@Rule
public final GriffonFestRule fest = new GriffonFestRule()
private FrameFixture window
@Test
void smokeTest() {
fail('Not implemented yet!')
}
}

View File

@@ -0,0 +1,21 @@
package com.muwire.gui
import griffon.core.test.GriffonUnitRule
import griffon.core.test.TestFor
import org.junit.Rule
import org.junit.Test
import static org.junit.Assert.fail
@TestFor(SearchTabController)
class SearchTabControllerTest {
private SearchTabController controller
@Rule
public final GriffonUnitRule griffon = new GriffonUnitRule()
@Test
void smokeTest() {
fail('Not yet implemented!')
}
}