Compare commits

...

43 Commits

Author SHA1 Message Date
Zlatin Balevsky
bc3b6f500f 0.0.5 for trust panel 2019-06-02 12:18:44 +01:00
Zlatin Balevsky
8f8710801c update any result tabs on trust events 2019-06-02 12:16:28 +01:00
Zlatin Balevsky
43f3cf9b7a small ui tweak 2019-06-02 12:00:14 +01:00
Zlatin Balevsky
6fe4155678 delete accidental commit 2019-06-02 11:57:15 +01:00
Zlatin Balevsky
32f944a089 trust panel ui 2019-06-02 11:56:19 +01:00
Zlatin Balevsky
b19b5ef315 Fix for java 9+ #1 2019-06-02 10:04:27 +01:00
Zlatin Balevsky
5138935c20 add options for portable installation, issue #2 2019-06-02 09:33:28 +01:00
Zlatin Balevsky
ba596af778 Trust panel, wip 2019-06-02 05:40:44 +01:00
Zlatin Balevsky
0f4533c867 persist personas in trust files instead of destinations 2019-06-02 05:12:14 +01:00
Zlatin Balevsky
727834390c slightly better looking message 2019-06-02 04:18:15 +01:00
Zlatin Balevsky
c51e3874da show a message instead of search bar while disconnected 2019-06-02 04:12:11 +01:00
Zlatin Balevsky
d18a618575 focus on the tab of the new search 2019-06-02 03:54:34 +01:00
Zlatin Balevsky
15508f417d hack to add some horizontal space 2019-06-02 01:33:53 +01:00
Zlatin Balevsky
44dad55178 update test 2019-06-02 01:28:00 +01:00
Zlatin Balevsky
5c17e77190 change groovy version to match griffon 2019-06-02 01:20:55 +01:00
Zlatin Balevsky
de856cd085 canonize search terms 2019-06-02 00:42:18 +01:00
Zlatin Balevsky
d2533cc4d6 retry failed downloads, every 15 minutes by default 2019-06-02 00:22:33 +01:00
Zlatin Balevsky
f41cc39659 show who is downloading 2019-06-01 21:53:14 +01:00
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
38 changed files with 779 additions and 112 deletions

View File

@@ -3,7 +3,7 @@ subprojects {
dependencies {
compile 'net.i2p:i2p:0.9.40'
compile 'org.codehaus.groovy:groovy-all:2.5.7'
compile 'org.codehaus.groovy:groovy-all:2.4.15'
}
compileGroovy {

View File

@@ -10,4 +10,6 @@ class Constants {
public static final int MAX_HEADERS = 16
public static final float DOWNLOAD_SEQUENTIAL_RATIO = 0.8f
public static final String SPLIT_PATTERN = "[\\.,_-]"
}

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()
@@ -118,8 +120,8 @@ public class Core {
eventBus = new EventBus()
log.info("initializing trust service")
File goodTrust = new File(home, "trust.good")
File badTrust = new File(home, "trust.bad")
File goodTrust = new File(home, "trusted")
File badTrust = new File(home, "distrusted")
trustService = new TrustService(goodTrust, badTrust, 5000)
eventBus.register(TrustEvent.class, trustService)
@@ -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"), me)
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

@@ -6,6 +6,7 @@ class MuWireSettings {
final boolean isLeaf
boolean allowUntrusted
int downloadRetryInterval
String nickname
File downloadLocation
String sharedFiles
@@ -23,6 +24,7 @@ class MuWireSettings {
downloadLocation = new File((String)props.getProperty("downloadLocation",
System.getProperty("user.home")))
sharedFiles = props.getProperty("sharedFiles")
downloadRetryInterval = Integer.parseInt(props.getProperty("downloadRetryInterval","15"))
}
void write(OutputStream out) throws IOException {
@@ -32,6 +34,7 @@ class MuWireSettings {
props.setProperty("crawlerResponse", crawlerResponse.toString())
props.setProperty("nickname", nickname)
props.setProperty("downloadLocation", downloadLocation.getAbsolutePath())
props.setProperty("downloadRetryInterval", "15")
if (sharedFiles != null)
props.setProperty("sharedFiles", sharedFiles)
props.store(out, "")

View File

@@ -2,6 +2,7 @@ package com.muwire.core
import net.i2p.crypto.DSAEngine
import net.i2p.crypto.SigType
import net.i2p.data.Base64
import net.i2p.data.Destination
import net.i2p.data.Signature
import net.i2p.data.SigningPublicKey
@@ -14,6 +15,7 @@ public class Persona {
private final Destination destination
private final byte[] sig
private volatile String humanReadableName
private volatile String base64
private volatile byte[] payload
public Persona(InputStream personaStream) throws IOException, InvalidSignatureException {
@@ -59,6 +61,15 @@ public class Persona {
humanReadableName
}
public String toBase64() {
if (base64 == null) {
def baos = new ByteArrayOutputStream()
write(baos)
base64 = Base64.encode(baos.toByteArray())
}
base64
}
@Override
public int hashCode() {
name.hashCode() ^ destination.hashCode()

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

@@ -40,7 +40,7 @@ abstract class ConnectionManager {
void onTrustEvent(TrustEvent e) {
if (e.level == TrustLevel.DISTRUSTED)
drop(e.destination)
drop(e.persona.destination)
}
abstract void drop(Destination d)
@@ -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

@@ -1,7 +1,11 @@
package com.muwire.core.download
import com.muwire.core.connection.I2PConnector
import net.i2p.data.Base64
import com.muwire.core.EventBus
import com.muwire.core.Persona
import java.util.concurrent.Executor
import java.util.concurrent.Executors
@@ -11,10 +15,20 @@ public class DownloadManager {
private final EventBus eventBus
private final I2PConnector connector
private final Executor executor
private final File incompletes
private final String meB64
public DownloadManager(EventBus eventBus, I2PConnector connector) {
public DownloadManager(EventBus eventBus, I2PConnector connector, File incompletes, Persona me) {
this.eventBus = eventBus
this.connector = connector
this.incompletes = incompletes
def baos = new ByteArrayOutputStream()
me.write(baos)
this.meB64 = Base64.encode(baos.toByteArray())
incompletes.mkdir()
this.executor = Executors.newCachedThreadPool({ r ->
Thread rv = new Thread(r)
rv.setName("download-worker")
@@ -25,9 +39,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, meB64, 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

@@ -20,6 +20,7 @@ import java.security.NoSuchAlgorithmException
@Log
class DownloadSession {
private final String meB64
private final Pieces pieces
private final InfoHash infoHash
private final Endpoint endpoint
@@ -30,8 +31,9 @@ class DownloadSession {
private ByteBuffer mapped
DownloadSession(Pieces pieces, InfoHash infoHash, Endpoint endpoint, File file,
DownloadSession(String meB64, Pieces pieces, InfoHash infoHash, Endpoint endpoint, File file,
int pieceSize, long fileLength) {
this.meB64 = meB64
this.pieces = pieces
this.endpoint = endpoint
this.infoHash = infoHash
@@ -60,7 +62,8 @@ class DownloadSession {
FileChannel channel
try {
os.write("GET $root\r\n".getBytes(StandardCharsets.US_ASCII))
os.write("Range: $start-$end\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
os.write("Range: $start-$end\r\n".getBytes(StandardCharsets.US_ASCII))
os.write("X-Persona: $meB64\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
os.flush()
String code = readTillRN(is)
if (code.startsWith("404 ")) {
@@ -127,11 +130,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,21 @@ 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 String meB64
private final File file
private final Pieces pieces
private final long length
@@ -18,17 +25,25 @@ 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, String meB64, File file, long length, InfoHash infoHash,
int pieceSizePow2, I2PConnector connector, Destination destination,
File incompletes) {
this.meB64 = meB64
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 +58,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(meB64, 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 +112,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

@@ -1,5 +1,6 @@
package com.muwire.core.search
import com.muwire.core.Constants
class SearchIndex {
@@ -31,7 +32,7 @@ class SearchIndex {
}
private static String[] split(String source) {
source = source.replaceAll("[\\.,_-]", " ")
source = source.replaceAll(Constants.SPLIT_PATTERN, " ").toLowerCase()
source.split(" ")
}

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,11 +1,10 @@
package com.muwire.core.trust
import com.muwire.core.Event
import net.i2p.data.Destination
import com.muwire.core.Persona
class TrustEvent extends Event {
Destination destination
Persona persona
TrustLevel level
}

View File

@@ -1,7 +1,11 @@
package com.muwire.core.trust
import java.util.concurrent.ConcurrentHashMap
import com.muwire.core.Persona
import com.muwire.core.Service
import net.i2p.data.Base64
import net.i2p.data.Destination
import net.i2p.util.ConcurrentHashSet
@@ -10,8 +14,8 @@ class TrustService extends Service {
final File persistGood, persistBad
final long persistInterval
final Set<Destination> good = new ConcurrentHashSet<>()
final Set<Destination> bad = new ConcurrentHashSet<>()
final Map<Destination, Persona> good = new ConcurrentHashMap<>()
final Map<Destination, Persona> bad = new ConcurrentHashMap<>()
final Timer timer
@@ -35,12 +39,16 @@ class TrustService extends Service {
void load() {
if (persistGood.exists()) {
persistGood.eachLine {
good.add(new Destination(it))
byte [] decoded = Base64.decode(it)
Persona persona = new Persona(new ByteArrayInputStream(decoded))
good.put(persona.destination, persona)
}
}
if (persistBad.exists()) {
persistBad.eachLine {
bad.add(new Destination(it))
byte [] decoded = Base64.decode(it)
Persona persona = new Persona(new ByteArrayInputStream(decoded))
bad.put(persona.destination, persona)
}
}
timer.schedule({persist()} as TimerTask, persistInterval, persistInterval)
@@ -50,22 +58,22 @@ class TrustService extends Service {
private void persist() {
persistGood.delete()
persistGood.withPrintWriter { writer ->
good.each {
writer.println it.toBase64()
good.each {k,v ->
writer.println v.toBase64()
}
}
persistBad.delete()
persistBad.withPrintWriter { writer ->
bad.each {
writer.println it.toBase64()
bad.each { k,v ->
writer.println v.toBase64()
}
}
}
TrustLevel getLevel(Destination dest) {
if (good.contains(dest))
if (good.containsKey(dest))
return TrustLevel.TRUSTED
else if (bad.contains(dest))
else if (bad.containsKey(dest))
return TrustLevel.DISTRUSTED
TrustLevel.NEUTRAL
}
@@ -73,16 +81,16 @@ class TrustService extends Service {
void onTrustEvent(TrustEvent e) {
switch(e.level) {
case TrustLevel.TRUSTED:
bad.remove(e.destination)
good.add(e.destination)
bad.remove(e.persona.destination)
good.put(e.persona.destination, e.persona)
break
case TrustLevel.DISTRUSTED:
good.remove(e.destination)
bad.add(e.destination)
good.remove(e.persona.destination)
bad.put(e.persona.destination, e.persona)
break
case TrustLevel.NEUTRAL:
good.remove(e.destination)
bad.remove(e.destination)
good.remove(e.persona.destination)
bad.remove(e.persona.destination)
break
}
}

View File

@@ -4,6 +4,7 @@ import java.nio.charset.StandardCharsets
import com.muwire.core.Constants
import com.muwire.core.InfoHash
import com.muwire.core.Persona
import groovy.util.logging.Log
import net.i2p.data.Base64
@@ -16,6 +17,7 @@ class Request {
InfoHash infoHash
Range range
Persona downloader
Map<String, String> headers
static Request parse(InfoHash infoHash, InputStream is) throws IOException {
@@ -85,7 +87,13 @@ class Request {
if (start < 0 || end < start)
throw new IOException("Invalid range $start - $end")
new Request( infoHash : infoHash, range : new Range(start, end), headers : headers)
Persona downloader = null
if (headers.containsKey("X-Persona")) {
def encoded = headers["X-Persona"].trim()
def decoded = Base64.decode(encoded)
downloader = new Persona(new ByteArrayInputStream(decoded))
}
new Request( infoHash : infoHash, range : new Range(start, end), headers : headers, downloader : downloader)
}
}

View File

@@ -55,7 +55,7 @@ class DownloadSessionTest {
toUploader = new PipedOutputStream(fromDownloader)
endpoint = new Endpoint(null, fromUploader, toUploader, null)
session = new DownloadSession(pieces, infoHash, endpoint, target, pieceSize, size)
session = new DownloadSession("",pieces, infoHash, endpoint, target, pieceSize, size)
downloadThread = new Thread( { session.request() } as Runnable)
downloadThread.setDaemon(true)
downloadThread.start()
@@ -74,6 +74,7 @@ class DownloadSessionTest {
initSession(20)
assert "GET $rootBase64" == readTillRN(fromDownloader)
assert "Range: 0-19" == readTillRN(fromDownloader)
readTillRN(fromDownloader)
assert "" == readTillRN(fromDownloader)
toDownloader.write("200 OK\r\n".bytes)
@@ -95,6 +96,7 @@ class DownloadSessionTest {
assert "GET $rootBase64" == readTillRN(fromDownloader)
readTillRN(fromDownloader)
readTillRN(fromDownloader)
assert "" == readTillRN(fromDownloader)
toDownloader.write("200 OK\r\n".bytes)
@@ -122,6 +124,7 @@ class DownloadSessionTest {
assert (start == 0 && end == ((1 << pieceSize) - 1)) ||
(start == (1 << pieceSize) && end == (1 << pieceSize))
readTillRN(fromDownloader)
assert "" == readTillRN(fromDownloader)
toDownloader.write("200 OK\r\n".bytes)

View File

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

View File

@@ -58,6 +58,7 @@ dependencies {
compile "org.codehaus.griffon:griffon-guice:${griffon.version}"
runtime "org.slf4j:slf4j-simple:${slf4jVersion}"
runtime "javax.annotation:javax.annotation-api:1.3.2"
testCompile "org.codehaus.griffon:griffon-fest-test:${griffon.version}"
testCompile "org.spockframework:spock-core:${spockVersion}"

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,11 +3,14 @@ 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
import javax.inject.Inject
import com.muwire.core.Constants
import com.muwire.core.Core
import com.muwire.core.download.DownloadStartedEvent
import com.muwire.core.download.UIDownloadEvent
@@ -29,21 +32,44 @@ class MainFrameController {
@ControllerAction
void search() {
def cardsPanel = builder.getVariable("cards-panel")
cardsPanel.getLayout().show(cardsPanel, "search window")
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
// this can be improved a lot
def terms = search.toLowerCase().trim().split(Constants.SPLIT_PATTERN)
def searchEvent = new SearchEvent(searchTerms : terms, 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,13 +77,56 @@ class MainFrameController {
@ControllerAction
void trust() {
def result = selectedResult()
core.eventBus.publish( new TrustEvent(destination : result.sender.destination, level : TrustLevel.TRUSTED))
if (result == null)
return // TODO disable button
core.eventBus.publish( new TrustEvent(persona : result.sender, level : TrustLevel.TRUSTED))
}
@ControllerAction
void distrust() {
def result = selectedResult()
core.eventBus.publish( new TrustEvent(destination : result.sender.destination, level : TrustLevel.DISTRUSTED))
if (result == null)
return // TODO disable button
core.eventBus.publish( new TrustEvent(persona : result.sender, level : TrustLevel.DISTRUSTED))
}
@ControllerAction
void cancel() {
def downloader = selectedDownload()
downloader.cancel()
}
@ControllerAction
void resume() {
def downloader = selectedDownload()
downloader.resume()
}
private void markTrust(String tableName, TrustLevel level, def list) {
int row = builder.getVariable(tableName).getSelectedRow()
if (row < 0)
return
core.eventBus.publish(new TrustEvent(persona : list[row], level : level))
}
@ControllerAction
void markTrusted() {
markTrust("distrusted-table", TrustLevel.TRUSTED, model.distrusted)
}
@ControllerAction
void markNeutralFromDistrusted() {
markTrust("distrusted-table", TrustLevel.NEUTRAL, model.distrusted)
}
@ControllerAction
void markDistrusted() {
markTrust("trusted-table", TrustLevel.DISTRUSTED, model.trusted)
}
@ControllerAction
void markNeutralFromTrusted() {
markTrust("trusted-table", TrustLevel.NEUTRAL, model.trusted)
}
void mvcGroupInit(Map<String, String> args) {

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 {
@@ -27,7 +28,11 @@ class Ready extends AbstractLifecycleHandler {
@Override
void execute() {
log.info "starting core services"
def home = System.getProperty("user.home") + File.separator + ".MuWire"
def portableHome = System.getProperty("portable.home")
def home = portableHome == null ?
System.getProperty("user.home") + File.separator + ".MuWire" :
portableHome
home = new File(home)
if (!home.exists()) {
log.info("creating home dir")
@@ -63,8 +68,13 @@ class Ready extends AbstractLifecycleHandler {
nickname = nickname.trim()
break
}
props.setNickname(nickname)
while(true) {
def portableDownloads = System.getProperty("portable.downloads")
if (portableDownloads != null) {
props.downloadLocation = new File(portableDownloads)
} else {
def chooser = new JFileChooser()
chooser.setDialogTitle("Select a directory where downloads will be saved")
chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY)
@@ -74,15 +84,21 @@ class Ready extends AbstractLifecycleHandler {
System.exit(0)
}
props.downloadLocation = chooser.getSelectedFile()
break
}
props.setNickname(nickname)
propsFile.withOutputStream {
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
@@ -10,9 +12,11 @@ import com.muwire.core.connection.ConnectionAttemptStatus
import com.muwire.core.connection.ConnectionEvent
import com.muwire.core.connection.DisconnectionEvent
import com.muwire.core.download.DownloadStartedEvent
import com.muwire.core.download.Downloader
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 +25,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,18 +39,47 @@ 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()
def trusted = []
def distrusted = []
@Observable int connections
@Observable String me
@Observable boolean searchButtonsEnabled
@Observable boolean cancelButtonEnabled
@Observable boolean retryButtonEnabled
private final Set<InfoHash> infoHashes = new HashSet<>()
volatile Core core
void updateTablePreservingSelection(String tableName) {
def downloadTable = builder.getVariable(tableName)
int selectedRow = downloadTable.getSelectedRow()
downloadTable.model.fireTableDataChanged()
downloadTable.selectionModel.setSelectionInterval(selectedRow,selectedRow)
}
void mvcGroupInit(Map<String, Object> args) {
Timer timer = new Timer("download-pumper", true)
timer.schedule({
runInsideUIAsync {
if (!mvcGroup.alive)
return
builder.getVariable("uploads-table")?.model.fireTableDataChanged()
updateTablePreservingSelection("downloads-table")
updateTablePreservingSelection("trusted-table")
updateTablePreservingSelection("distrusted-table")
}
}, 1000, 1000)
application.addPropertyChangeListener("core", {e ->
coreInitialized = (e.getNewValue() != null)
core = e.getNewValue()
@@ -58,22 +93,32 @@ class MainFrameModel {
core.eventBus.register(UploadEvent.class, this)
core.eventBus.register(UploadFinishedEvent.class, this)
core.eventBus.register(TrustEvent.class, this)
})
Timer timer = new Timer("download-pumper", true)
timer.schedule({
runInsideUIAsync {
builder.getVariable("downloads-table").model.fireTableDataChanged()
builder.getVariable("uploads-table").model.fireTableDataChanged()
core.eventBus.register(QueryEvent.class, this)
int retryInterval = application.context.get("muwire-settings").downloadRetryInterval
if (retryInterval > 0) {
retryInterval *= 60000
timer.schedule({
runInsideUIAsync {
downloads.each {
if (it.downloader.currentState == Downloader.DownloadState.FAILED)
it.downloader.resume()
}
}
}, retryInterval, retryInterval)
}
}, 1000, 1000)
runInsideUIAsync {
trusted.addAll(core.trustService.good.values())
distrusted.addAll(core.trustService.bad.values())
}
})
}
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 +128,34 @@ class MainFrameModel {
}
void onConnectionEvent(ConnectionEvent e) {
if (e.getStatus() != ConnectionAttemptStatus.SUCCESSFUL)
return
runInsideUIAsync {
connections = core.connectionManager.getConnections().size()
if (connections > 0) {
def topPanel = builder.getVariable("top-panel")
topPanel.getLayout().show(topPanel, "top-search-panel")
}
connectionList.add(e.endpoint.destination)
JTable table = builder.getVariable("connections-table")
table.model.fireTableDataChanged()
}
}
void onDisconnectionEvent(DisconnectionEvent e) {
runInsideUIAsync {
connections = core.connectionManager.getConnections().size()
if (connections == 0) {
def topPanel = builder.getVariable("top-panel")
topPanel.getLayout().show(topPanel, "top-connect-panel")
}
connectionList.remove(e.destination)
JTable table = builder.getVariable("connections-table")
table.model.fireTableDataChanged()
}
}
@@ -136,7 +201,37 @@ class MainFrameModel {
void onTrustEvent(TrustEvent e) {
runInsideUIAsync {
JTable table = builder.getVariable("results-table")
trusted.clear()
trusted.addAll(core.trustService.good.values())
distrusted.clear()
distrusted.addAll(core.trustService.bad.values())
updateTablePreservingSelection("trusted-table")
updateTablePreservingSelection("distrusted-table")
results.values().each {
it.view.pane.getClientProperty("results-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
@@ -20,6 +22,7 @@ import java.awt.FlowLayout
import java.awt.GridBagConstraints
import java.awt.GridBagLayout
import java.awt.Insets
import java.nio.charset.StandardCharsets
import javax.annotation.Nonnull
@@ -48,15 +51,25 @@ class MainFrameView {
gridLayout(rows:1, cols: 2)
button(text: "Searches", actionPerformed : showSearchWindow)
button(text: "Uploads", actionPerformed : showUploadsWindow)
button(text: "Monitor", actionPerformed : showMonitorWindow)
button(text: "Trust", actionPerformed : showTrustWindow)
}
panel(constraints: BorderLayout.CENTER) {
borderLayout()
label("Enter search here:", constraints: BorderLayout.WEST)
textField(id: "search-field", constraints: BorderLayout.CENTER, action : searchAction)
}
panel( constraints: BorderLayout.EAST) {
button(text: "Search", searchAction)
panel(id: "top-panel", constraints: BorderLayout.CENTER) {
cardLayout()
label(constraints : "top-connect-panel",
text : " MuWire is connecting, please wait. You will be able to search soon.") // TODO: real padding
panel(constraints : "top-search-panel") {
borderLayout()
panel(constraints: BorderLayout.CENTER) {
borderLayout()
label(" Enter search here:", constraints: BorderLayout.WEST) // TODO: fix this
textField(id: "search-field", constraints: BorderLayout.CENTER, action : searchAction)
}
panel( constraints: BorderLayout.EAST) {
button(text: "Search", searchAction)
}
}
}
}
panel (id: "cards-panel", constraints : BorderLayout.CENTER) {
@@ -67,22 +80,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 +107,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 +119,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 +132,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) {
@@ -138,11 +146,76 @@ class MainFrameView {
int percent = (int)((position * 100.0) / total)
"$percent%"
})
closureColumn(header : "Downloader", type : String, read : { row ->
row.request.downloader?.getHumanReadableName()
})
}
}
}
}
}
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(constraints : "trust window") {
gridLayout(rows: 1, cols :2)
panel (border : etchedBorder()){
borderLayout()
scrollPane(constraints : BorderLayout.CENTER) {
table(id : "trusted-table") {
tableModel(list : model.trusted) {
closureColumn(header : "Trusted Users", type : String, read : { it.getHumanReadableName() } )
}
}
}
panel (constraints : BorderLayout.EAST) {
gridBagLayout()
button(text : "Mark Neutral", constraints : gbc(gridx: 0, gridy: 0), markNeutralFromTrustedAction)
button(text : "Mark Distrusted", constraints : gbc(gridx: 0, gridy:1), markDistrustedAction)
}
}
panel (border : etchedBorder()){
borderLayout()
scrollPane(constraints : BorderLayout.CENTER) {
table(id : "distrusted-table") {
tableModel(list : model.distrusted) {
closureColumn(header: "Distrusted Users", type : String, read : { it.getHumanReadableName() } )
}
}
}
panel(constraints : BorderLayout.WEST) {
gridBagLayout()
button(text: "Mark Neutral", constraints: gbc(gridx: 0, gridy: 0), markNeutralFromDistrustedAction)
button(text: "Mark Trusted", constraints : gbc(gridx: 0, gridy : 1), markTrustedAction)
}
}
}
}
panel (border: etchedBorder(), constraints : BorderLayout.SOUTH) {
borderLayout()
@@ -156,6 +229,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 +264,16 @@ class MainFrameView {
cardsPanel.getLayout().show(cardsPanel, "uploads window")
}
def showMonitorWindow = {
def cardsPanel = builder.getVariable("cards-panel")
cardsPanel.getLayout().show(cardsPanel,"monitor window")
}
def showTrustWindow = {
def cardsPanel = builder.getVariable("cards-panel")
cardsPanel.getLayout().show(cardsPanel,"trust window")
}
def shareFiles = {
def chooser = new JFileChooser()
chooser.setDialogTitle("Select file or directory to share")

View File

@@ -0,0 +1,81 @@
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)
parent.setSelectedIndex(index)
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!')
}
}