Compare commits

...

67 Commits

Author SHA1 Message Date
Zlatin Balevsky
0db7077a45 Release 0.2.4 2019-06-17 03:22:52 +01:00
Zlatin Balevsky
614ecc85fe new piece selection logic to avoid high cpu bug 2019-06-17 03:21:37 +01:00
Zlatin Balevsky
af66a79376 fix sorting by progress 2019-06-17 00:56:16 +01:00
Zlatin Balevsky
465171c81d prevent multiple identical shared files 2019-06-17 00:38:05 +01:00
Zlatin Balevsky
b507361c58 close the file before marking pieces complete 2019-06-16 23:45:23 +01:00
Zlatin Balevsky
4d001ae74b thread-safe access to the pieces file 2019-06-16 22:56:09 +01:00
Zlatin Balevsky
36a6e2769f Release 0.2.3 2019-06-16 19:05:12 +01:00
Zlatin Balevsky
69eeb7d77a fix 2019-06-16 18:58:52 +01:00
Zlatin Balevsky
551982b72a batch results sent to the GUI to prevent freeze 2019-06-16 18:51:07 +01:00
Zlatin Balevsky
8d808f0b8f Release 0.2.2 2019-06-16 13:30:11 +01:00
Zlatin Balevsky
7833a83c87 mark hash queries for V2 results 2019-06-16 13:17:32 +01:00
Zlatin Balevsky
3160c1a8f3 fix for silent uploader exceptions 2019-06-16 13:01:14 +01:00
Zlatin Balevsky
e295aa67d5 proper log statement 2019-06-16 10:59:11 +01:00
Zlatin Balevsky
a9f5625dc3 fix popup menu on failed downloads 2019-06-16 10:50:21 +01:00
Zlatin Balevsky
cc0af5b9ed add context menu to downloads table 2019-06-16 10:29:28 +01:00
Zlatin Balevsky
041fc3bef3 Release 0.2.1 2019-06-16 09:37:53 +01:00
Zlatin Balevsky
03c3b1ebf1 fix copying of hash if search results are sorted 2019-06-16 09:30:52 +01:00
Zlatin Balevsky
aece390daa right-click menu on the search results tab 2019-06-16 09:17:17 +01:00
Zlatin Balevsky
cf63be68e8 copy search to clipboard 2019-06-16 08:38:47 +01:00
Zlatin Balevsky
88ece4dc23 add option to show search hashes in monitor 2019-06-16 08:29:03 +01:00
Zlatin Balevsky
13767d58f2 detect if a query is hash, get rid of radio buttons 2019-06-16 08:09:51 +01:00
Zlatin Balevsky
05a1ccd3d8 update todo 2019-06-16 07:31:01 +01:00
Zlatin Balevsky
6807c14a5f add copy hash to clipboard 2019-06-16 07:23:22 +01:00
Zlatin Balevsky
684be0c50e start of work on directory watcher 2019-06-16 07:03:16 +01:00
Zlatin Balevsky
6655c262c6 more todo items 2019-06-16 07:01:50 +01:00
Zlatin Balevsky
b1ccd55030 more todo items 2019-06-16 06:26:03 +01:00
Zlatin Balevsky
a3becd0f7e update TODO 2019-06-16 06:19:28 +01:00
Zlatin Balevsky
af2f3e0ebf in/out direction done 2019-06-16 05:56:56 +01:00
Zlatin Balevsky
e2b7ffa1db direction in monitor tab 2019-06-16 05:52:23 +01:00
Zlatin Balevsky
0e0176acfc add web UI to TODO list 2019-06-16 05:35:05 +01:00
Zlatin Balevsky
7f09bb079c Beginnings of a TODO list 2019-06-16 05:28:42 +01:00
Zlatin Balevsky
77e48b01bb Release 0.2.0 2019-06-15 21:10:11 +01:00
Zlatin Balevsky
12db6857c1 disable unshare files popup until implemented 2019-06-15 12:12:08 +01:00
Zlatin Balevsky
acd67733a5 sort the downloads table on updates 2019-06-15 12:08:29 +01:00
Zlatin Balevsky
8d3ce7aa8e use the same sorted row selection logic in downloads table 2019-06-15 09:57:12 +01:00
Zlatin Balevsky
0eb5870e9b Release 0.1.13 2019-06-15 09:19:19 +01:00
Zlatin Balevsky
051efbfaba prevent empty searches 2019-06-15 09:11:42 +01:00
Zlatin Balevsky
6b38d7bffb fix sorting bug try 2 2019-06-15 08:58:51 +01:00
Zlatin Balevsky
5778d537ce Release 0.1.12 2019-06-15 08:39:19 +01:00
Zlatin Balevsky
93664a7985 update readme 2019-06-15 08:37:29 +01:00
Zlatin Balevsky
edd58e0c90 allow cancelling of downloads while hashlist is being fetched 2019-06-15 08:35:23 +01:00
Zlatin Balevsky
9ac52b61dc sort results table on update 2019-06-15 08:33:22 +01:00
Zlatin Balevsky
0a4b9c7029 shut down connection manager last 2019-06-15 08:20:10 +01:00
Zlatin Balevsky
87b366a205 add ability to cancel failed downloads 2019-06-14 22:49:56 +01:00
Zlatin Balevsky
040248560a Release 0.1.11 2019-06-14 22:26:28 +01:00
Zlatin Balevsky
77caaf83de reset instead of close 2019-06-14 22:08:25 +01:00
Zlatin Balevsky
cc5ece5103 do not throw exception on shutdown 2019-06-14 21:36:50 +01:00
Zlatin Balevsky
db7e21e343 close connections in parallel, more shutdown fixes 2019-06-14 21:25:22 +01:00
Zlatin Balevsky
a388eaec1d shutdown all connections on shutdown 2019-06-14 20:53:54 +01:00
Zlatin Balevsky
8ff39072c7 download file on double-clicking a result 2019-06-14 20:42:26 +01:00
Zlatin Balevsky
55d2ac9b24 delete partial files and pieces file on cancel 2019-06-14 20:27:14 +01:00
Zlatin Balevsky
6ebe492fd8 if nothing is enabled cancel and retry buttons are disabled 2019-06-14 18:37:18 +01:00
Zlatin Balevsky
165cd542ec work around not having a selected row while cancelling a download 2019-06-14 18:28:00 +01:00
Zlatin Balevsky
5ca0c8b00d wip on unshare selected files popup menu 2019-06-14 18:08:56 +01:00
Zlatin Balevsky
b6a38e3f23 revert to default lnf if the desired one fails 2019-06-14 18:01:14 +01:00
Zlatin Balevsky
34d9165bd5 Release 0.1.10 2019-06-14 16:43:28 +01:00
Zlatin Balevsky
2e52dd5c49 fix overwriting of custom nickname 2019-06-14 16:20:21 +01:00
Zlatin Balevsky
2a315dd734 add option to exclude local results from searches 2019-06-14 14:48:01 +01:00
Zlatin Balevsky
6b661b99c5 fix sorting by size in shared files table 2019-06-14 13:47:35 +01:00
Zlatin Balevsky
5dacd60bbb hook up cleaning up of cancelled/finished downloads 2019-06-14 13:11:20 +01:00
Zlatin Balevsky
f8f7cfe836 UI options panel 2019-06-14 12:51:27 +01:00
Zlatin Balevsky
0b4f261bc1 ability to not show monitor panel 2019-06-14 12:21:14 +01:00
Zlatin Balevsky
042d67d784 fix selection of size column 2019-06-14 11:46:31 +01:00
Zlatin Balevsky
800df88f14 proper sorting by size 2019-06-14 11:10:19 +01:00
Zlatin Balevsky
4d1eac50a0 update readme for sorting bug 2019-06-14 10:39:58 +01:00
Zlatin Balevsky
c48df7f14b Release 0.1.9 2019-06-13 22:57:08 +01:00
Zlatin Balevsky
9d04148001 remember loaded downloads from previous sessions 2019-06-13 22:53:23 +01:00
32 changed files with 728 additions and 148 deletions

View File

@@ -31,4 +31,3 @@ The first time you run MuWire it will ask you to select a nickname. This nickna
### Known bugs and limitations
* Many UI features you would expect are not there yet

42
TODO.md Normal file
View File

@@ -0,0 +1,42 @@
# TODO List
Not in any particular order yet
### Big Items
##### Alternate Locations
This helps peers discover new sources for a file while the download is in progress. Also makes sharing of partial files possible.
##### Bloom Filters
This reduces query traffic by not sending last hop queries to peers that definitely do not have the file
##### Two-tier Topology
This helps with scalability
##### Trust List Sharing
For helping users make better decisions whom to trust
##### Content Control Panel
To allow every user to not route queries for content they do not like. This is mostly GUI work, the backend part is simple
##### Packaging With JRE, Embedded Router
For ease of deployment for new users, and so that users do not need to run a separate I2P router
##### Web UI, REST Interface, etc.
Basically any non-gui non-cli user interface
### Small Items
* Detect if router is dead and show warning or exit
* Wrapper of some kind for in-place upgrades
* Download file sequentially
* Unsharing of files
* Multiple-selection download, Ctrl-A
* Automatic sharing of new files in shared directories (more like medium item)

View File

@@ -34,7 +34,7 @@ class Cli {
Core core
try {
core = new Core(props, home, "0.1.8")
core = new Core(props, home, "0.2.4")
} catch (Exception bad) {
bad.printStackTrace(System.out)
println "Failed to initialize core, exiting"

View File

@@ -53,7 +53,7 @@ class CliDownloader {
Core core
try {
core = new Core(props, home, "0.1.8")
core = new Core(props, home, "0.2.4")
} catch (Exception bad) {
bad.printStackTrace(System.out)
println "Failed to initialize core, exiting"

View File

@@ -69,6 +69,7 @@ public class Core {
private final ConnectionAcceptor connectionAcceptor
private final ConnectionEstablisher connectionEstablisher
private final HasherService hasherService
private final DownloadManager downloadManager
public Core(MuWireSettings props, File home, String myVersion) {
this.home = home
@@ -92,9 +93,9 @@ public class Core {
if (i2pOptionsFile.exists()) {
i2pOptionsFile.withInputStream { i2pOptions.load(it) }
if (!i2pOptions.hasProperty("inbound.nickname"))
if (!i2pOptions.containsKey("inbound.nickname"))
i2pOptions["inbound.nickname"] = "MuWire"
if (!i2pOptions.hasProperty("outbound.nickname"))
if (!i2pOptions.containsKey("outbound.nickname"))
i2pOptions["outbound.nickname"] = "MuWire"
} else {
i2pOptions["inbound.nickname"] = "MuWire"
@@ -195,7 +196,7 @@ public class Core {
eventBus.register(ResultsEvent.class, searchManager)
log.info("initializing download manager")
DownloadManager downloadManager = new DownloadManager(eventBus, i2pConnector, home, me)
downloadManager = new DownloadManager(eventBus, i2pConnector, home, me)
eventBus.register(UIDownloadEvent.class, downloadManager)
eventBus.register(UILoadedEvent.class, downloadManager)
eventBus.register(FileDownloadedEvent.class, downloadManager)
@@ -233,6 +234,13 @@ public class Core {
}
public void shutdown() {
log.info("shutting down download manageer")
downloadManager.shutdown()
log.info("shutting down connection acceeptor")
connectionAcceptor.stop()
log.info("shutting down connection establisher")
connectionEstablisher.stop()
log.info("shutting down connection manager")
connectionManager.shutdown()
}
@@ -260,7 +268,7 @@ public class Core {
}
}
Core core = new Core(props, home, "0.1.8")
Core core = new Core(props, home, "0.2.4")
core.startServices()
// ... at the end, sleep or execute script

View File

@@ -13,6 +13,7 @@ class MuWireSettings {
String sharedFiles
CrawlerResponse crawlerResponse
boolean shareDownloadedFiles
boolean watchSharedDirectories
MuWireSettings() {
this(new Properties())
@@ -29,6 +30,7 @@ class MuWireSettings {
downloadRetryInterval = Integer.parseInt(props.getProperty("downloadRetryInterval","15"))
updateCheckInterval = Integer.parseInt(props.getProperty("updateCheckInterval","36"))
shareDownloadedFiles = Boolean.parseBoolean(props.getProperty("shareDownloadedFiles","true"))
watchSharedDirectories = Boolean.parseBoolean(props.getProperty("watchSharedDirectories","true"))
}
void write(OutputStream out) throws IOException {
@@ -41,6 +43,7 @@ class MuWireSettings {
props.setProperty("downloadRetryInterval", String.valueOf(downloadRetryInterval))
props.setProperty("updateCheckInterval", String.valueOf(updateCheckInterval))
props.setProperty("shareDownloadedFiles", String.valueOf(shareDownloadedFiles))
props.setProperty("watchSharedDirectories", String.valueOf(watchSharedDirectories))
if (sharedFiles != null)
props.setProperty("sharedFiles", sharedFiles)
props.store(out, "")

View File

@@ -76,9 +76,9 @@ abstract class Connection implements Closeable {
return
}
log.info("closing $name")
endpoint.close()
reader.interrupt()
writer.interrupt()
endpoint.close()
eventBus.publish(new DisconnectionEvent(destination: endpoint.destination))
}

View File

@@ -17,6 +17,8 @@ import com.muwire.core.upload.UploadManager
import com.muwire.core.search.InvalidSearchResultException
import com.muwire.core.search.ResultsParser
import com.muwire.core.search.SearchManager
import com.muwire.core.search.UIResultBatchEvent
import com.muwire.core.search.UIResultEvent
import com.muwire.core.search.UnexpectedResultsException
import groovy.json.JsonOutput
@@ -38,6 +40,8 @@ class ConnectionAcceptor {
final ExecutorService acceptorThread
final ExecutorService handshakerThreads
private volatile shutdown
ConnectionAcceptor(EventBus eventBus, UltrapeerConnectionManager manager,
MuWireSettings settings, I2PAcceptor acceptor, HostCache hostCache,
@@ -73,11 +77,13 @@ class ConnectionAcceptor {
}
void stop() {
shutdown = true
acceptorThread.shutdownNow()
handshakerThreads.shutdownNow()
}
private void acceptLoop() {
try {
while(true) {
def incoming = acceptor.accept()
log.info("accepted connection from ${incoming.destination.toBase32()}")
@@ -93,6 +99,11 @@ class ConnectionAcceptor {
}
handshakerThreads.execute({processIncoming(incoming)} as Runnable)
}
} catch (Exception e) {
log.log(Level.WARNING, "exception in accept loop",e)
if (!shutdown)
throw e
}
}
private void processIncoming(Endpoint e) {
@@ -216,13 +227,15 @@ class ConnectionAcceptor {
if (sender.destination != e.getDestination())
throw new IOException("Sender destination mismatch expected $e.getDestination(), got $sender.destination")
int nResults = dis.readUnsignedShort()
UIResultEvent[] results = new UIResultEvent[nResults]
for (int i = 0; i < nResults; i++) {
int jsonSize = dis.readUnsignedShort()
byte [] payload = new byte[jsonSize]
dis.readFully(payload)
def json = slurper.parse(payload)
eventBus.publish(ResultsParser.parse(sender, resultsUUID, json))
results[i] = ResultsParser.parse(sender, resultsUUID, json)
}
eventBus.publish(new UIResultBatchEvent(uuid: resultsUUID, results: results))
} catch (IOException | UnexpectedResultsException | InvalidSearchResultException bad) {
log.log(Level.WARNING, "failed to process POST", bad)
} finally {

View File

@@ -1,6 +1,7 @@
package com.muwire.core.connection
import java.util.concurrent.atomic.AtomicBoolean
import java.util.logging.Level
import groovy.util.logging.Log
import net.i2p.data.Destination
@@ -24,7 +25,7 @@ class Endpoint implements Closeable {
@Override
public void close() {
if (!closed.compareAndSet(false, true)) {
log.warning("Close loop detected for ${destination.toBase32()}", new Exception())
log.log(Level.WARNING,"Close loop detected for ${destination.toBase32()}", new Exception())
return
}
if (inputStream != null) {
@@ -34,7 +35,7 @@ class Endpoint implements Closeable {
try {outputStream.close()} catch (Exception ignore) {}
}
if (toClose != null) {
try {toClose.close()} catch (Exception ignore) {}
try {toClose.reset()} catch (Exception ignore) {}
}
}

View File

@@ -104,8 +104,8 @@ class UltrapeerConnectionManager extends ConnectionManager {
@Override
void shutdown() {
peerConnections.each {k,v -> v.close() }
leafConnections.each {k,v -> v.close() }
peerConnections.values().stream().parallel().forEach({v -> v.close()})
leafConnections.values().stream().parallel().forEach({v -> v.close()})
peerConnections.clear()
leafConnections.clear()
}

View File

@@ -99,6 +99,7 @@ public class DownloadManager {
}
def downloader = new Downloader(eventBus, this, me, file, (long)json.length,
infoHash, json.pieceSizePow2, connector, destinations, incompletes)
downloaders.add(downloader)
downloader.download()
eventBus.publish(new DownloadStartedEvent(downloader : downloader))
}
@@ -134,4 +135,9 @@ public class DownloadManager {
}
}
}
public void shutdown() {
downloaders.each { it.stop() }
Downloader.executorService.shutdownNow()
}
}

View File

@@ -16,6 +16,7 @@ import java.nio.file.Files
import java.nio.file.StandardOpenOption
import java.security.MessageDigest
import java.security.NoSuchAlgorithmException
import java.util.logging.Level
@Log
class DownloadSession {
@@ -23,7 +24,7 @@ class DownloadSession {
private static int SAMPLES = 10
private final String meB64
private final Pieces downloaded, claimed
private final Pieces pieces
private final InfoHash infoHash
private final Endpoint endpoint
private final File file
@@ -36,11 +37,10 @@ class DownloadSession {
private ByteBuffer mapped
DownloadSession(String meB64, Pieces downloaded, Pieces claimed, 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.downloaded = downloaded
this.claimed = claimed
this.pieces = pieces
this.endpoint = endpoint
this.infoHash = infoHash
this.file = file
@@ -63,20 +63,11 @@ class DownloadSession {
OutputStream os = endpoint.getOutputStream()
InputStream is = endpoint.getInputStream()
int piece
while(true) {
piece = downloaded.getRandomPiece()
if (claimed.isMarked(piece)) {
if (downloaded.donePieces() + claimed.donePieces() == downloaded.nPieces) {
log.info("all pieces claimed")
return false
}
continue
}
break
}
claimed.markDownloaded(piece)
int piece = pieces.claim()
if (piece == -1)
return false
boolean unclaim = true
log.info("will download piece $piece")
long start = piece * pieceSize
@@ -85,7 +76,6 @@ class DownloadSession {
String root = Base64.encode(infoHash.getRoot())
FileChannel channel
try {
os.write("GET $root\r\n".getBytes(StandardCharsets.US_ASCII))
os.write("Range: $start-$end\r\n".getBytes(StandardCharsets.US_ASCII))
@@ -135,41 +125,46 @@ class DownloadSession {
}
// start the download
channel = Files.newByteChannel(file.toPath(), EnumSet.of(StandardOpenOption.READ, StandardOpenOption.WRITE,
StandardOpenOption.SPARSE, StandardOpenOption.CREATE)) // TODO: double-check, maybe CREATE_NEW
mapped = channel.map(FileChannel.MapMode.READ_WRITE, start, end - start + 1)
byte[] tmp = new byte[0x1 << 13]
while(mapped.hasRemaining()) {
if (mapped.remaining() < tmp.length)
tmp = new byte[mapped.remaining()]
int read = is.read(tmp)
if (read == -1)
throw new IOException()
synchronized(this) {
mapped.put(tmp, 0, read)
if (timestamps.size() == SAMPLES) {
timestamps.removeFirst()
reads.removeFirst()
FileChannel channel
try {
channel = Files.newByteChannel(file.toPath(), EnumSet.of(StandardOpenOption.READ, StandardOpenOption.WRITE,
StandardOpenOption.SPARSE, StandardOpenOption.CREATE)) // TODO: double-check, maybe CREATE_NEW
mapped = channel.map(FileChannel.MapMode.READ_WRITE, start, end - start + 1)
byte[] tmp = new byte[0x1 << 13]
while(mapped.hasRemaining()) {
if (mapped.remaining() < tmp.length)
tmp = new byte[mapped.remaining()]
int read = is.read(tmp)
if (read == -1)
throw new IOException()
synchronized(this) {
mapped.put(tmp, 0, read)
if (timestamps.size() == SAMPLES) {
timestamps.removeFirst()
reads.removeFirst()
}
timestamps.addLast(System.currentTimeMillis())
reads.addLast(read)
}
timestamps.addLast(System.currentTimeMillis())
reads.addLast(read)
}
mapped.clear()
digest.update(mapped)
byte [] hash = digest.digest()
byte [] expected = new byte[32]
System.arraycopy(infoHash.getHashList(), piece * 32, expected, 0, 32)
if (hash != expected)
throw new BadHashException()
} finally {
try { channel?.close() } catch (IOException ignore) {}
}
mapped.clear()
digest.update(mapped)
byte [] hash = digest.digest()
byte [] expected = new byte[32]
System.arraycopy(infoHash.getHashList(), piece * 32, expected, 0, 32)
if (hash != expected)
throw new BadHashException()
downloaded.markDownloaded(piece)
pieces.markDownloaded(piece)
unclaim = false
} finally {
claimed.clear(piece)
try { channel?.close() } catch (IOException ignore) {}
if (unclaim)
pieces.unclaim(piece)
}
return true
}

View File

@@ -7,6 +7,7 @@ import com.muwire.core.connection.Endpoint
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import java.util.concurrent.atomic.AtomicBoolean
import java.util.logging.Level
import com.muwire.core.Constants
@@ -34,7 +35,7 @@ public class Downloader {
private final DownloadManager downloadManager
private final Persona me
private final File file
private final Pieces downloaded, claimed
private final Pieces pieces
private final long length
private InfoHash infoHash
private final int pieceSize
@@ -47,7 +48,8 @@ public class Downloader {
private volatile boolean cancelled
private volatile boolean eventFired
private final AtomicBoolean eventFired = new AtomicBoolean()
private boolean piecesFileClosed
public Downloader(EventBus eventBus, DownloadManager downloadManager,
Persona me, File file, long length, InfoHash infoHash,
@@ -72,8 +74,7 @@ public class Downloader {
nPieces = length / pieceSize + 1
this.nPieces = nPieces
downloaded = new Pieces(nPieces, Constants.DOWNLOAD_SEQUENTIAL_RATIO)
claimed = new Pieces(nPieces)
pieces = new Pieces(nPieces, Constants.DOWNLOAD_SEQUENTIAL_RATIO)
}
public synchronized InfoHash getInfoHash() {
@@ -100,20 +101,24 @@ public class Downloader {
return
piecesFile.eachLine {
int piece = Integer.parseInt(it)
downloaded.markDownloaded(piece)
pieces.markDownloaded(piece)
}
}
void writePieces() {
piecesFile.withPrintWriter { writer ->
downloaded.getDownloaded().each { piece ->
writer.println(piece)
synchronized(piecesFile) {
if (piecesFileClosed)
return
piecesFile.withPrintWriter { writer ->
pieces.getDownloaded().each { piece ->
writer.println(piece)
}
}
}
}
public long donePieces() {
downloaded.donePieces()
pieces.donePieces()
}
@@ -136,7 +141,7 @@ public class Downloader {
allFinished &= it.currentState == WorkerState.FINISHED
}
if (allFinished) {
if (downloaded.isComplete())
if (pieces.isComplete())
return DownloadState.FINISHED
return DownloadState.FAILED
}
@@ -169,6 +174,15 @@ public class Downloader {
public void cancel() {
cancelled = true
stop()
file.delete()
synchronized(piecesFile) {
piecesFileClosed = true
piecesFile.delete()
}
}
void stop() {
activeWorkers.values().each {
it.cancel()
}
@@ -225,8 +239,8 @@ public class Downloader {
}
currentState = WorkerState.DOWNLOADING
boolean requestPerformed
while(!downloaded.isComplete()) {
currentSession = new DownloadSession(me.toBase64(), downloaded, claimed, getInfoHash(), endpoint, file, pieceSize, length)
while(!pieces.isComplete()) {
currentSession = new DownloadSession(me.toBase64(), pieces, getInfoHash(), endpoint, file, pieceSize, length)
requestPerformed = currentSession.request()
if (!requestPerformed)
break
@@ -236,9 +250,11 @@ public class Downloader {
log.log(Level.WARNING,"Exception while downloading",bad)
} finally {
currentState = WorkerState.FINISHED
if (downloaded.isComplete() && !eventFired) {
piecesFile.delete()
eventFired = true
if (pieces.isComplete() && eventFired.compareAndSet(false, true)) {
synchronized(piecesFile) {
piecesFileClosed = true
piecesFile.delete()
}
eventBus.publish(
new FileDownloadedEvent(
downloadedFile : new DownloadedFile(file, getInfoHash(), pieceSizePow2, Collections.emptySet()),

View File

@@ -1,7 +1,7 @@
package com.muwire.core.download
class Pieces {
private final BitSet bitSet
private final BitSet done, claimed
private final int nPieces
private final float ratio
private final Random random = new Random()
@@ -13,52 +13,53 @@ class Pieces {
Pieces(int nPieces, float ratio) {
this.nPieces = nPieces
this.ratio = ratio
bitSet = new BitSet(nPieces)
done = new BitSet(nPieces)
claimed = new BitSet(nPieces)
}
synchronized int getRandomPiece() {
int cardinality = bitSet.cardinality()
if (cardinality == nPieces)
synchronized int claim() {
int claimedCardinality = claimed.cardinality()
if (claimedCardinality == nPieces)
return -1
// if fuller than ratio just do sequential
if ( (1.0f * cardinality) / nPieces > ratio) {
return bitSet.nextClearBit(0)
if ( (1.0f * claimedCardinality) / nPieces > ratio) {
int rv = claimed.nextClearBit(0)
claimed.set(rv)
return rv
}
while(true) {
int start = random.nextInt(nPieces)
if (bitSet.get(start))
if (claimed.get(start))
continue
claimed.set(start)
return start
}
}
def getDownloaded() {
synchronized def getDownloaded() {
def rv = []
for (int i = bitSet.nextSetBit(0); i >= 0; i = bitSet.nextSetBit(i+1)) {
for (int i = done.nextSetBit(0); i >= 0; i = done.nextSetBit(i+1)) {
rv << i
}
rv
}
synchronized void markDownloaded(int piece) {
bitSet.set(piece)
done.set(piece)
claimed.set(piece)
}
synchronized void clear(int piece) {
bitSet.clear(piece)
synchronized void unclaim(int piece) {
claimed.clear(piece)
}
synchronized boolean isComplete() {
bitSet.cardinality() == nPieces
}
synchronized boolean isMarked(int piece) {
bitSet.get(piece)
done.cardinality() == nPieces
}
synchronized int donePieces() {
bitSet.cardinality()
done.cardinality()
}
}

View File

@@ -0,0 +1,4 @@
package com.muwire.core.files
class DirectoryWatcher {
}

View File

@@ -0,0 +1,8 @@
package com.muwire.core.search
import com.muwire.core.Event
class UIResultBatchEvent extends Event {
UUID uuid
UIResultEvent[] results
}

View File

@@ -51,4 +51,23 @@ class ContentUploader extends Uploader {
}
}
@Override
public String getName() {
return file.getName();
}
@Override
public synchronized int getProgress() {
if (mapped == null)
return 0
int position = mapped.position()
int total = request.getRange().end - request.getRange().start
(int)(position * 100.0 / total)
}
@Override
public String getDownloader() {
request.downloader.getHumanReadableName()
}
}

View File

@@ -6,6 +6,8 @@ import java.nio.charset.StandardCharsets
import com.muwire.core.InfoHash
import com.muwire.core.connection.Endpoint
import net.i2p.data.Base64
class HashListUploader extends Uploader {
private final InfoHash infoHash
private final HashListRequest request
@@ -14,6 +16,7 @@ class HashListUploader extends Uploader {
super(endpoint)
this.infoHash = infoHash
mapped = ByteBuffer.wrap(infoHash.getHashList())
this.request = request
}
void respond() {
@@ -32,4 +35,21 @@ class HashListUploader extends Uploader {
}
endpoint.getOutputStream().flush()
}
@Override
public String getName() {
return "Hash list for " + Base64.encode(infoHash.getRoot());
}
@Override
public synchronized int getProgress() {
(int)(mapped.position() * 100.0 / mapped.capacity())
}
@Override
public String getDownloader() {
request.downloader.getHumanReadableName()
}
}

View File

@@ -23,4 +23,13 @@ abstract class Uploader {
return -1
mapped.position()
}
abstract String getName();
/**
* @return an integer between 0 and 100
*/
abstract int getProgress();
abstract String getDownloader();
}

View File

@@ -25,4 +25,17 @@ public class SharedFile {
public int getPieceSize() {
return pieceSize;
}
@Override
public int hashCode() {
return file.hashCode() ^ infoHash.hashCode();
}
@Override
public boolean equals(Object o) {
if (!(o instanceof SharedFile))
return false;
SharedFile other = (SharedFile)o;
return file.equals(other.file) && infoHash.equals(other.infoHash);
}
}

View File

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

View File

@@ -39,6 +39,9 @@ class MainFrameController {
cardsPanel.getLayout().show(cardsPanel, "search window")
def search = builder.getVariable("search-field").text
search = search.trim()
if (search.length() == 0)
return
def uuid = UUID.randomUUID()
Map<String, Object> params = new HashMap<>()
params["search-terms"] = search
@@ -46,9 +49,20 @@ class MainFrameController {
def group = mvcGroup.createMVCGroup("SearchTab", uuid.toString(), params)
model.results[uuid.toString()] = group
boolean hashSearch = false
byte [] root = null
if (search.length() == 44 && search.indexOf(" ") < 0) {
try {
root = Base64.decode(search)
hashSearch = true
} catch (Exception e) {
// not a hash search
}
}
def searchEvent
if (model.hashSearch) {
searchEvent = new SearchEvent(searchHash : Base64.decode(search), uuid : uuid)
if (hashSearch) {
searchEvent = new SearchEvent(searchHash : root, uuid : uuid, oobInfohash: true)
} else {
// this can be improved a lot
def replaced = search.toLowerCase().trim().replaceAll(Constants.SPLIT_PATTERN, " ")
@@ -86,16 +100,17 @@ class MainFrameController {
return
def sortEvt = group.view.lastSortEvent
if (sortEvt != null) {
row = sortEvt.convertPreviousRowIndexToModel(row)
}
row = group.view.resultsTable.rowSorter.convertRowIndexToModel(row)
}
group.model.results[row]
}
private int selectedDownload() {
def selected = builder.getVariable("downloads-table").getSelectedRow()
def downloadsTable = builder.getVariable("downloads-table")
def selected = downloadsTable.getSelectedRow()
def sortEvt = mvcGroup.view.lastDownloadSortEvent
if (sortEvt != null)
selected = sortEvt.convertPreviousRowIndexToModel(selected)
selected = downloadsTable.rowSorter.convertRowIndexToModel(selected)
selected
}
@@ -171,14 +186,8 @@ class MainFrameController {
markTrust("trusted-table", TrustLevel.NEUTRAL, model.trusted)
}
@ControllerAction
void keywordSearch() {
model.hashSearch = false
}
@ControllerAction
void hashSearch() {
model.hashSearch = true
void unshareSelectedFiles() {
println "unsharing selected files"
}
void mvcGroupInit(Map<String, String> args) {

View File

@@ -66,6 +66,42 @@ class OptionsController {
settings.write(it)
}
// UI Setttings
UISettings uiSettings = application.context.get("ui-settings")
text = view.lnfField.text
model.lnf = text
uiSettings.lnf = text
text = view.fontField.text
model.font = text
uiSettings.font = text
boolean showMonitor = view.monitorCheckbox.model.isSelected()
model.showMonitor = showMonitor
uiSettings.showMonitor = showMonitor
boolean clearCancelledDownloads = view.clearCancelledDownloadsCheckbox.model.isSelected()
model.clearCancelledDownloads = clearCancelledDownloads
uiSettings.clearCancelledDownloads = clearCancelledDownloads
boolean clearFinishedDownloads = view.clearFinishedDownloadsCheckbox.model.isSelected()
model.clearFinishedDownloads = clearFinishedDownloads
uiSettings.clearFinishedDownloads = clearFinishedDownloads
boolean excludeLocalResult = view.excludeLocalResultCheckbox.model.isSelected()
model.excludeLocalResult = excludeLocalResult
uiSettings.excludeLocalResult = excludeLocalResult
boolean showSearchHashes = view.showSearchHashesCheckbox.model.isSelected()
model.showSearchHashes = showSearchHashes
uiSettings.showSearchHashes = showSearchHashes
File uiSettingsFile = new File(core.home, "gui.properties")
uiSettingsFile.withOutputStream {
uiSettings.write(it)
}
cancel()
}

View File

@@ -18,6 +18,7 @@ import static griffon.util.GriffonApplicationUtils.isMacOSX
import static groovy.swing.SwingBuilder.lookAndFeel
import java.awt.Font
import java.util.logging.Level
@Log
class Initialize extends AbstractLifecycleHandler {
@@ -50,7 +51,12 @@ class Initialize extends AbstractLifecycleHandler {
uiSettings = new UISettings(props)
log.info("settting user-specified lnf $uiSettings.lnf")
lookAndFeel(uiSettings.lnf)
try {
lookAndFeel(uiSettings.lnf)
} catch (Throwable bad) {
log.log(Level.WARNING,"couldn't set desired look and feeel, switching to defaults", bad)
uiSettings.lnf = lookAndFeel("system","gtk","metal").getID()
}
if (uiSettings.font != null) {
log.info("setting user-specified font $uiSettings.font")
@@ -78,7 +84,7 @@ class Initialize extends AbstractLifecycleHandler {
}
} else {
LookAndFeel chosen = lookAndFeel('system', 'gtk')
uiSettings.lnf = chosen.name
uiSettings.lnf = chosen.getID()
log.info("ended up applying $chosen.name")
}
}

View File

@@ -20,6 +20,7 @@ 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.UIResultBatchEvent
import com.muwire.core.search.UIResultEvent
import com.muwire.core.trust.TrustEvent
import com.muwire.core.trust.TrustService
@@ -34,6 +35,7 @@ import griffon.core.mvc.MVCGroup
import griffon.inject.MVCMember
import griffon.transform.FXObservable
import griffon.transform.Observable
import net.i2p.data.Base64
import net.i2p.data.Destination
import griffon.metadata.ArtifactProviderFor
@@ -56,8 +58,6 @@ class MainFrameModel {
def trusted = []
def distrusted = []
boolean hashSearch
@Observable int connections
@Observable String me
@Observable boolean searchButtonsEnabled
@@ -69,6 +69,8 @@ class MainFrameModel {
volatile Core core
private long lastRetryTime = System.currentTimeMillis()
UISettings uiSettings
void updateTablePreservingSelection(String tableName) {
def downloadTable = builder.getVariable(tableName)
@@ -79,11 +81,28 @@ class MainFrameModel {
void mvcGroupInit(Map<String, Object> args) {
uiSettings = application.context.get("ui-settings")
Timer timer = new Timer("download-pumper", true)
timer.schedule({
runInsideUIAsync {
if (!mvcGroup.alive)
return
// remove cancelled or finished downloads
def toRemove = []
downloads.each {
if (uiSettings.clearCancelledDownloads &&
it.downloader.getCurrentState() == Downloader.DownloadState.CANCELLED)
toRemove << it
if (uiSettings.clearFinishedDownloads &&
it.downloader.getCurrentState() == Downloader.DownloadState.FINISHED)
toRemove << it
}
toRemove.each {
downloads.remove(it)
}
builder.getVariable("uploads-table")?.model.fireTableDataChanged()
updateTablePreservingSelection("downloads-table")
@@ -97,6 +116,7 @@ class MainFrameModel {
core = e.getNewValue()
me = core.me.getHumanReadableName()
core.eventBus.register(UIResultEvent.class, this)
core.eventBus.register(UIResultBatchEvent.class, this)
core.eventBus.register(DownloadStartedEvent.class, this)
core.eventBus.register(ConnectionEvent.class, this)
core.eventBus.register(DisconnectionEvent.class, this)
@@ -143,6 +163,11 @@ class MainFrameModel {
resultsGroup?.model.handleResult(e)
}
void onUIResultBatchEvent(UIResultBatchEvent e) {
MVCGroup resultsGroup = results.get(e.uuid)
resultsGroup?.model.handleResultBatch(e.results)
}
void onDownloadStartedEvent(DownloadStartedEvent e) {
runInsideUIAsync {
downloads << e
@@ -160,7 +185,8 @@ class MainFrameModel {
topPanel.getLayout().show(topPanel, "top-search-panel")
}
connectionList.add(e.endpoint.destination)
UIConnection con = new UIConnection(destination : e.endpoint.destination, incoming : e.incoming)
connectionList.add(con)
JTable table = builder.getVariable("connections-table")
table.model.fireTableDataChanged()
}
@@ -175,7 +201,8 @@ class MainFrameModel {
topPanel.getLayout().show(topPanel, "top-connect-panel")
}
connectionList.remove(e.destination)
UIConnection con = new UIConnection(destination : e.destination)
connectionList.remove(con)
JTable table = builder.getVariable("connections-table")
table.model.fireTableDataChanged()
}
@@ -241,14 +268,23 @@ class MainFrameModel {
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
if (e.searchEvent.searchHash != null) {
if (!uiSettings.showSearchHashes) {
return
}
search = Base64.encode(e.searchEvent.searchHash)
} else {
StringBuilder sb = new StringBuilder()
e.searchEvent.searchTerms?.each {
sb.append(it)
sb.append(" ")
}
search = sb.toString()
if (search.trim().size() == 0)
return
}
def search = sb.toString()
if (search.trim().size() == 0)
return
runInsideUIAsync {
searches.addFirst(new IncomingSearch(search : search, replyTo : e.replyTo, originator : e.originator))
while(searches.size() > 200)
@@ -286,4 +322,22 @@ class MainFrameModel {
table.model.fireTableDataChanged()
}
}
private static class UIConnection {
Destination destination
boolean incoming
@Override
public int hashCode() {
destination.hashCode()
}
@Override
public boolean equals(Object o) {
if (!(o instanceof UIConnection))
return false
UIConnection other = (UIConnection) o
return destination == other.destination
}
}
}

View File

@@ -20,6 +20,15 @@ class OptionsModel {
@Observable String outboundLength
@Observable String outboundQuantity
// gui options
@Observable boolean showMonitor
@Observable String lnf
@Observable String font
@Observable boolean clearCancelledDownloads
@Observable boolean clearFinishedDownloads
@Observable boolean excludeLocalResult
@Observable boolean showSearchHashes
void mvcGroupInit(Map<String, String> args) {
MuWireSettings settings = application.context.get("muwire-settings")
downloadRetryInterval = settings.downloadRetryInterval
@@ -32,5 +41,14 @@ class OptionsModel {
inboundQuantity = core.i2pOptions["inbound.quantity"]
outboundLength = core.i2pOptions["outbound.length"]
outboundQuantity = core.i2pOptions["outbound.quantity"]
UISettings uiSettings = application.context.get("ui-settings")
showMonitor = uiSettings.showMonitor
lnf = uiSettings.lnf
font = uiSettings.font
clearCancelledDownloads = uiSettings.clearCancelledDownloads
clearFinishedDownloads = uiSettings.clearFinishedDownloads
excludeLocalResult = uiSettings.excludeLocalResult
showSearchHashes = uiSettings.showSearchHashes
}
}

View File

@@ -19,6 +19,7 @@ class SearchTabModel {
FactoryBuilderSupport builder
Core core
UISettings uiSettings
String uuid
def results = []
def hashBucket = [:]
@@ -26,6 +27,7 @@ class SearchTabModel {
void mvcGroupInit(Map<String, String> args) {
core = mvcGroup.parentGroup.model.core
uiSettings = application.context.get("ui-settings")
mvcGroup.parentGroup.model.results[UUID.fromString(uuid)] = mvcGroup
}
@@ -34,6 +36,9 @@ class SearchTabModel {
}
void handleResult(UIResultEvent e) {
if (uiSettings.excludeLocalResult &&
e.sender == core.me)
return
runInsideUIAsync {
def bucket = hashBucket.get(e.infohash)
if (bucket == null) {
@@ -47,4 +52,22 @@ class SearchTabModel {
table.model.fireTableDataChanged()
}
}
void handleResultBatch(UIResultEvent[] batch) {
runInsideUIAsync {
batch.each {
if (uiSettings.excludeLocalResult && it.sender == core.me)
return
def bucket = hashBucket.get(it.infohash)
if (bucket == null) {
bucket = []
hashBucket[it.infohash] = bucket
}
bucket << it
results << it
}
JTable table = builder.getVariable("results-table")
table.model.fireTableDataChanged()
}
}
}

View File

@@ -3,6 +3,7 @@ package com.muwire.gui
import griffon.core.artifact.GriffonView
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import net.i2p.data.Base64
import net.i2p.data.DataHelper
import javax.swing.BorderFactory
@@ -10,7 +11,10 @@ import javax.swing.Box
import javax.swing.BoxLayout
import javax.swing.JFileChooser
import javax.swing.JLabel
import javax.swing.JMenuItem
import javax.swing.JPopupMenu
import javax.swing.JSplitPane
import javax.swing.JTable
import javax.swing.ListSelectionModel
import javax.swing.SwingConstants
import javax.swing.border.Border
@@ -26,6 +30,10 @@ import java.awt.FlowLayout
import java.awt.GridBagConstraints
import java.awt.GridBagLayout
import java.awt.Insets
import java.awt.Toolkit
import java.awt.datatransfer.StringSelection
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
import java.nio.charset.StandardCharsets
import javax.annotation.Nonnull
@@ -39,8 +47,10 @@ class MainFrameView {
def downloadsTable
def lastDownloadSortEvent
def lastSharedSortEvent
void initUI() {
UISettings settings = application.context.get("ui-settings")
builder.with {
application(size : [1024,768], id: 'main-frame',
locationRelativeTo : null,
@@ -63,7 +73,8 @@ class MainFrameView {
gridLayout(rows:1, cols: 2)
button(text: "Searches", actionPerformed : showSearchWindow)
button(text: "Uploads", actionPerformed : showUploadsWindow)
button(text: "Monitor", actionPerformed : showMonitorWindow)
if (settings.showMonitor)
button(text: "Monitor", actionPerformed : showMonitorWindow)
button(text: "Trust", actionPerformed : showTrustWindow)
}
panel(id: "top-panel", constraints: BorderLayout.CENTER) {
@@ -79,12 +90,6 @@ class MainFrameView {
}
panel( constraints: BorderLayout.EAST) {
panel {
buttonGroup(id : "searchButtonGroup")
radioButton(text : "Keywords", selected : true, buttonGroup : searchButtonGroup, keywordSearchAction)
radioButton(text : "Hash", selected : false, buttonGroup : searchButtonGroup, hashSearchAction)
}
button(text: "Search", searchAction)
}
}
@@ -115,7 +120,7 @@ class MainFrameView {
closureColumn(header: "Progress", preferredWidth: 20, type: String, read: { row ->
int pieces = row.downloader.nPieces
int done = row.downloader.donePieces()
"$done/$pieces pieces"
"$done/$pieces pieces".toString()
})
closureColumn(header: "Sources", preferredWidth : 10, type: Integer, read : {row -> row.downloader.activeWorkers()})
closureColumn(header: "Speed", preferredWidth: 50, type:String, read :{row ->
@@ -142,8 +147,7 @@ class MainFrameView {
table(id : "shared-files-table", autoCreateRowSorter: true) {
tableModel(list : model.shared) {
closureColumn(header : "Name", preferredWidth : 550, type : String, read : {row -> row.file.getAbsolutePath()})
closureColumn(header : "Size", preferredWidth : 50, type : String,
read : {row -> DataHelper.formatSize2Decimal(row.file.length(),false) + "B"})
closureColumn(header : "Size", preferredWidth : 50, type : Long, read : {row -> row.file.length() })
}
}
}
@@ -156,16 +160,13 @@ class MainFrameView {
scrollPane (constraints : BorderLayout.CENTER) {
table(id : "uploads-table") {
tableModel(list : model.uploads) {
closureColumn(header : "Name", type : String, read : {row -> row.file.getName() })
closureColumn(header : "Name", type : String, read : {row -> row.getName() })
closureColumn(header : "Progress", type : String, read : { row ->
int position = row.getPosition()
def range = row.request.getRange()
int total = range.end - range.start
int percent = (int)((position * 100.0) / total)
int percent = row.getProgress()
"$percent%"
})
closureColumn(header : "Downloader", type : String, read : { row ->
row.request.downloader?.getHumanReadableName()
row.getDownloader()
})
}
}
@@ -182,7 +183,13 @@ class MainFrameView {
scrollPane(constraints : BorderLayout.CENTER) {
table(id : "connections-table") {
tableModel(list : model.connectionList) {
closureColumn(header : "Destination", type: String, read : { row -> row.toBase32() })
closureColumn(header : "Destination", preferredWidth: 250, type: String, read : { row -> row.destination.toBase32() })
closureColumn(header : "Direction", preferredWidth: 20, type: String, read : { row ->
if (row.incoming)
return "In"
else
return "Out"
})
}
}
}
@@ -264,15 +271,23 @@ class MainFrameView {
selectionModel.setSelectionMode(ListSelectionModel.SINGLE_SELECTION)
selectionModel.addListSelectionListener({
int selectedRow = selectedDownloaderRow()
def downloader = model.downloads[selectedRow].downloader
if (selectedRow < 0) {
model.cancelButtonEnabled = false
model.retryButtonEnabled = false
return
}
def downloader = model.downloads[selectedRow]?.downloader
if (downloader == null)
return
switch(downloader.getCurrentState()) {
case Downloader.DownloadState.CONNECTING :
case Downloader.DownloadState.DOWNLOADING :
case Downloader.DownloadState.HASHLIST:
model.cancelButtonEnabled = true
model.retryButtonEnabled = false
break
case Downloader.DownloadState.FAILED:
model.cancelButtonEnabled = false
model.cancelButtonEnabled = true
model.retryButtonEnabled = true
break
default:
@@ -286,6 +301,92 @@ class MainFrameView {
downloadsTable.setDefaultRenderer(Integer.class, centerRenderer)
downloadsTable.rowSorter.addRowSorterListener({evt -> lastDownloadSortEvent = evt})
downloadsTable.rowSorter.setSortsOnUpdates(true)
downloadsTable.addMouseListener(new MouseAdapter() {
@Override
public void mouseReleased(MouseEvent e) {
if (e.isPopupTrigger())
showDownloadsMenu(e)
}
@Override
public void mousePressed(MouseEvent e) {
if (e.isPopupTrigger())
showDownloadsMenu(e)
}
})
// shared files table
def sharedFilesTable = builder.getVariable("shared-files-table")
sharedFilesTable.columnModel.getColumn(1).setCellRenderer(new SizeRenderer())
sharedFilesTable.rowSorter.addRowSorterListener({evt -> lastSharedSortEvent = evt})
sharedFilesTable.rowSorter.setSortsOnUpdates(true)
JPopupMenu sharedFilesMenu = new JPopupMenu()
// JMenuItem unshareSelectedFiles = new JMenuItem("Unshare selected files")
// unshareSelectedFiles.addActionListener({mvcGroup.controller.unshareSelectedFiles()})
JMenuItem copyHashToClipboard = new JMenuItem("Copy hash to clipboard")
copyHashToClipboard.addActionListener({mvcGroup.view.copyHashToClipboard(sharedFilesTable)})
sharedFilesMenu.add(copyHashToClipboard)
sharedFilesTable.addMouseListener(new MouseAdapter() {
@Override
public void mouseReleased(MouseEvent e) {
if (e.isPopupTrigger())
showPopupMenu(sharedFilesMenu, e)
}
@Override
public void mousePressed(MouseEvent e) {
if (e.isPopupTrigger())
showPopupMenu(sharedFilesMenu, e)
}
})
// searches table
def searchesTable = builder.getVariable("searches-table")
JPopupMenu searchTableMenu = new JPopupMenu()
JMenuItem copySearchToClipboard = new JMenuItem("Copy search to clipboard")
copySearchToClipboard.addActionListener({mvcGroup.view.copySearchToClipboard(searchesTable)})
searchTableMenu.add(copySearchToClipboard)
searchesTable.addMouseListener(new MouseAdapter() {
@Override
public void mouseReleased(MouseEvent e) {
if (e.isPopupTrigger())
showPopupMenu(searchTableMenu, e)
}
@Override
public void mousePressed(MouseEvent e) {
if (e.isPopupTrigger())
showPopupMenu(searchTableMenu, e)
}
})
}
private static void showPopupMenu(JPopupMenu menu, MouseEvent event) {
menu.show(event.getComponent(), event.getX(), event.getY())
}
def copyHashToClipboard(JTable sharedFilesTable) {
int selected = sharedFilesTable.getSelectedRow()
if (selected < 0)
return
if (lastSharedSortEvent != null)
selected = sharedFilesTable.rowSorter.convertRowIndexToModel(selected)
String root = Base64.encode(model.shared[selected].infoHash.getRoot())
StringSelection selection = new StringSelection(root)
def clipboard = Toolkit.getDefaultToolkit().getSystemClipboard()
clipboard.setContents(selection, null)
}
def copySearchToClipboard(JTable searchesTable) {
int selected = searchesTable.getSelectedRow()
if (selected < 0)
return
String search = model.searches[selected].search
StringSelection selection = new StringSelection(search)
def clipboard = Toolkit.getDefaultToolkit().getSystemClipboard()
clipboard.setContents(selection, null)
}
int selectedDownloaderRow() {
@@ -295,6 +396,54 @@ class MainFrameView {
selected
}
def showDownloadsMenu(MouseEvent e) {
int selected = selectedDownloaderRow()
if (selected < 0)
return
boolean cancelEnabled = false
boolean retryEnabled = false
Downloader downloader = model.downloads[selected].downloader
switch(downloader.currentState) {
case Downloader.DownloadState.DOWNLOADING:
case Downloader.DownloadState.HASHLIST:
case Downloader.DownloadState.CONNECTING:
cancelEnabled = true
retryEnabled = false
break
case Downloader.DownloadState.FAILED:
cancelEnabled = true
retryEnabled = true
break
default :
cancelEnabled = false
retryEnabled = false
}
JPopupMenu menu = new JPopupMenu()
JMenuItem copyHashToClipboard = new JMenuItem("Copy hash to clipboard")
copyHashToClipboard.addActionListener({
String hash = Base64.encode(downloader.infoHash.getRoot())
StringSelection selection = new StringSelection(hash)
def clipboard = Toolkit.getDefaultToolkit().getSystemClipboard()
clipboard.setContents(selection, null)
})
menu.add(copyHashToClipboard)
if (cancelEnabled) {
JMenuItem cancel = new JMenuItem("Cancel")
cancel.addActionListener({mvcGroup.controller.cancel()})
menu.add(cancel)
}
if (retryEnabled) {
JMenuItem retry = new JMenuItem("Retry")
retry.addActionListener({mvcGroup.controller.resume()})
menu.add(retry)
}
showPopupMenu(menu, e)
}
def showSearchWindow = {
def cardsPanel = builder.getVariable("cards-panel")
cardsPanel.getLayout().show(cardsPanel, "search window")

View File

@@ -25,6 +25,8 @@ class OptionsView {
def d
def p
def i
def u
def retryField
def updateField
def allowUntrustedCheckbox
@@ -35,6 +37,14 @@ class OptionsView {
def outboundLengthField
def outboundQuantityField
def lnfField
def monitorCheckbox
def fontField
def clearCancelledDownloadsCheckbox
def clearFinishedDownloadsCheckbox
def excludeLocalResultCheckbox
def showSearchHashesCheckbox
def buttonsPanel
def mainFrame
@@ -72,6 +82,24 @@ class OptionsView {
label(text : "Outbound Quantity", constraints : gbc(gridx:0, gridy:4))
outboundQuantityField = textField(text : bind {model.outboundQuantity}, columns : 2, constraints : gbc(gridx:1, gridy:4))
}
u = builder.panel {
gridBagLayout()
label(text : "Changing these settings requires a restart", constraints : gbc(gridx : 0, gridy : 0, gridwidth: 2))
label(text : "Look And Feel", constraints : gbc(gridx: 0, gridy:1))
lnfField = textField(text : bind {model.lnf}, columns : 4, constraints : gbc(gridx : 1, gridy : 1))
label(text : "Font", constraints : gbc(gridx: 0, gridy : 2))
fontField = textField(text : bind {model.font}, columns : 4, constraints : gbc(gridx : 1, gridy:2))
label(text : "Show Monitor", constraints : gbc(gridx :0, gridy: 3))
monitorCheckbox = checkBox(selected : bind {model.showMonitor}, constraints : gbc(gridx : 1, gridy: 3))
label(text : "Clear Cancelled Downloads", constraints: gbc(gridx: 0, gridy:4))
clearCancelledDownloadsCheckbox = checkBox(selected : bind {model.clearCancelledDownloads}, constraints : gbc(gridx : 1, gridy:4))
label(text : "Clear Finished Downloads", constraints: gbc(gridx: 0, gridy:5))
clearFinishedDownloadsCheckbox = checkBox(selected : bind {model.clearFinishedDownloads}, constraints : gbc(gridx : 1, gridy:5))
label(text : "Exclude Local Files From Results", constraints: gbc(gridx:0, gridy:6))
excludeLocalResultCheckbox = checkBox(selected : bind {model.excludeLocalResult}, constraints : gbc(gridx: 1, gridy : 6))
label(text : "Show Hash Searches In Monitor", constraints: gbc(gridx:0, gridy:7))
showSearchHashesCheckbox = checkBox(selected : bind {model.showSearchHashes}, constraints : gbc(gridx: 1, gridy: 7))
}
buttonsPanel = builder.panel {
gridBagLayout()
button(text : "Save", constraints : gbc(gridx : 1, gridy: 2), saveAction)
@@ -81,8 +109,9 @@ class OptionsView {
void mvcGroupInit(Map<String,String> args) {
def tabbedPane = new JTabbedPane()
tabbedPane.addTab("MuWire Options", p)
tabbedPane.addTab("I2P Options", i)
tabbedPane.addTab("MuWire", p)
tabbedPane.addTab("I2P", i)
tabbedPane.addTab("GUI", u)
JPanel panel = new JPanel()
panel.setLayout(new BorderLayout())

View File

@@ -4,14 +4,26 @@ import griffon.core.artifact.GriffonView
import griffon.core.mvc.MVCGroup
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import net.i2p.data.Base64
import net.i2p.data.DataHelper
import javax.swing.JComponent
import javax.swing.JLabel
import javax.swing.JMenuItem
import javax.swing.JPopupMenu
import javax.swing.JTable
import javax.swing.ListSelectionModel
import javax.swing.SwingConstants
import javax.swing.table.DefaultTableCellRenderer
import com.muwire.core.util.DataUtil
import java.awt.BorderLayout
import java.awt.Color
import java.awt.Toolkit
import java.awt.datatransfer.StringSelection
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
import javax.annotation.Nonnull
@@ -35,7 +47,7 @@ class SearchTabView {
resultsTable = table(id : "results-table", autoCreateRowSorter : true) {
tableModel(list: model.results) {
closureColumn(header: "Name", preferredWidth: 350, type: String, read : {row -> row.name.replace('<','_')})
closureColumn(header: "Size", preferredWidth: 50, type: String, read : {row -> DataHelper.formatSize2Decimal(row.size, false)+"B"})
closureColumn(header: "Size", preferredWidth: 50, type: Long, read : {row -> row.size})
closureColumn(header: "Sources", preferredWidth: 10, type : Integer, read : { row -> model.hashBucket[row.infohash].size()})
closureColumn(header: "Sender", preferredWidth: 170, type: String, read : {row -> row.sender.getHumanReadableName()})
closureColumn(header: "Trust", preferredWidth: 50, type: String, read : {row ->
@@ -86,7 +98,34 @@ class SearchTabView {
resultsTable.setDefaultRenderer(Integer.class,centerRenderer)
resultsTable.columnModel.getColumn(4).setCellRenderer(centerRenderer)
resultsTable.columnModel.getColumn(1).setCellRenderer(new SizeRenderer())
resultsTable.rowSorter.addRowSorterListener({ evt -> lastSortEvent = evt})
resultsTable.rowSorter.setSortsOnUpdates(true)
JPopupMenu menu = new JPopupMenu()
JMenuItem download = new JMenuItem("Download")
download.addActionListener({mvcGroup.parentGroup.controller.download()})
menu.add(download)
JMenuItem copyHashToClipboard = new JMenuItem("Copy hash to clipboard")
copyHashToClipboard.addActionListener({mvcGroup.view.copyHashToClipboard()})
menu.add(copyHashToClipboard)
resultsTable.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
if (e.button == MouseEvent.BUTTON3)
showPopupMenu(menu, e)
else if (e.button == MouseEvent.BUTTON1 && e.clickCount == 2)
mvcGroup.parentGroup.controller.download()
}
@Override
public void mouseReleased(MouseEvent e) {
if (e.button == MouseEvent.BUTTON3)
showPopupMenu(menu, e)
}
})
}
def closeTab = {
@@ -95,4 +134,21 @@ class SearchTabView {
mvcGroup.parentGroup.model.searchButtonsEnabled = false
mvcGroup.destroy()
}
def showPopupMenu(JPopupMenu menu, MouseEvent e) {
println "showing popup menu"
menu.show(e.getComponent(), e.getX(), e.getY())
}
def copyHashToClipboard() {
int selected = resultsTable.getSelectedRow()
if (selected < 0)
return
if (lastSortEvent != null)
selected = resultsTable.rowSorter.convertRowIndexToModel(selected)
String hash = Base64.encode(model.results[selected].infohash.getRoot())
StringSelection selection = new StringSelection(hash)
def clipboard = Toolkit.getDefaultToolkit().getSystemClipboard()
clipboard.setContents(selection, null)
}
}

View File

@@ -0,0 +1,29 @@
package com.muwire.gui
import javax.swing.JComponent
import javax.swing.JLabel
import javax.swing.JTable
import javax.swing.table.DefaultTableCellRenderer
import net.i2p.data.DataHelper
class SizeRenderer extends DefaultTableCellRenderer {
SizeRenderer() {
setHorizontalAlignment(JLabel.CENTER)
}
@Override
JComponent getTableCellRendererComponent(JTable table, Object value,
boolean isSelected, boolean hasFocus, int row, int column) {
Long l = (Long) value
String formatted = DataHelper.formatSize2Decimal(l, false)+"B"
setText(formatted)
if (isSelected) {
setForeground(table.getSelectionForeground())
setBackground(table.getSelectionBackground())
} else {
setForeground(table.getForeground())
setBackground(table.getBackground())
}
this
}
}

View File

@@ -5,19 +5,33 @@ class UISettings {
String lnf
boolean showMonitor
String font
boolean clearCancelledDownloads
boolean clearFinishedDownloads
boolean excludeLocalResult
boolean showSearchHashes
UISettings(Properties props) {
lnf = props.getProperty("lnf", "system")
showMonitor = Boolean.parseBoolean(props.getProperty("showMonitor", "true"))
font = props.getProperty("font",null)
clearCancelledDownloads = Boolean.parseBoolean(props.getProperty("clearCancelledDownloads","false"))
clearFinishedDownloads = Boolean.parseBoolean(props.getProperty("clearFinishedDownloads","false"))
excludeLocalResult = Boolean.parseBoolean(props.getProperty("excludeLocalResult","false"))
showSearchHashes = Boolean.parseBoolean(props.getProperty("showSearchHashes","false"))
}
void write(OutputStream out) throws IOException {
Properties props = new Properties()
props.setProperty("lnf", lnf)
props.setProperty("showMonitor", showMonitor)
props.setProperty("showMonitor", String.valueOf(showMonitor))
props.setProperty("clearCancelledDownloads", String.valueOf(clearCancelledDownloads))
props.setProperty("clearFinishedDownloads", String.valueOf(clearFinishedDownloads))
props.setProperty("excludeLocalResult", String.valueOf(excludeLocalResult))
props.setProperty("showSearchHashes", String.valueOf(showSearchHashes))
if (font != null)
props.setProperty("font", font)
props.store(out, "UI Properties")
}
}