Compare commits
55 Commits
muwire-0.1
...
muwire-0.2
Author | SHA1 | Date | |
---|---|---|---|
![]() |
0db7077a45 | ||
![]() |
614ecc85fe | ||
![]() |
af66a79376 | ||
![]() |
465171c81d | ||
![]() |
b507361c58 | ||
![]() |
4d001ae74b | ||
![]() |
36a6e2769f | ||
![]() |
69eeb7d77a | ||
![]() |
551982b72a | ||
![]() |
8d808f0b8f | ||
![]() |
7833a83c87 | ||
![]() |
3160c1a8f3 | ||
![]() |
e295aa67d5 | ||
![]() |
a9f5625dc3 | ||
![]() |
cc0af5b9ed | ||
![]() |
041fc3bef3 | ||
![]() |
03c3b1ebf1 | ||
![]() |
aece390daa | ||
![]() |
cf63be68e8 | ||
![]() |
88ece4dc23 | ||
![]() |
13767d58f2 | ||
![]() |
05a1ccd3d8 | ||
![]() |
6807c14a5f | ||
![]() |
684be0c50e | ||
![]() |
6655c262c6 | ||
![]() |
b1ccd55030 | ||
![]() |
a3becd0f7e | ||
![]() |
af2f3e0ebf | ||
![]() |
e2b7ffa1db | ||
![]() |
0e0176acfc | ||
![]() |
7f09bb079c | ||
![]() |
77e48b01bb | ||
![]() |
12db6857c1 | ||
![]() |
acd67733a5 | ||
![]() |
8d3ce7aa8e | ||
![]() |
0eb5870e9b | ||
![]() |
051efbfaba | ||
![]() |
6b38d7bffb | ||
![]() |
5778d537ce | ||
![]() |
93664a7985 | ||
![]() |
edd58e0c90 | ||
![]() |
9ac52b61dc | ||
![]() |
0a4b9c7029 | ||
![]() |
87b366a205 | ||
![]() |
040248560a | ||
![]() |
77caaf83de | ||
![]() |
cc5ece5103 | ||
![]() |
db7e21e343 | ||
![]() |
a388eaec1d | ||
![]() |
8ff39072c7 | ||
![]() |
55d2ac9b24 | ||
![]() |
6ebe492fd8 | ||
![]() |
165cd542ec | ||
![]() |
5ca0c8b00d | ||
![]() |
b6a38e3f23 |
@@ -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
|
||||
* Sorting the results table sometimes causes the wrong result to be downloaded
|
||||
|
42
TODO.md
Normal file
42
TODO.md
Normal 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)
|
@@ -34,7 +34,7 @@ class Cli {
|
||||
|
||||
Core core
|
||||
try {
|
||||
core = new Core(props, home, "0.1.10")
|
||||
core = new Core(props, home, "0.2.4")
|
||||
} catch (Exception bad) {
|
||||
bad.printStackTrace(System.out)
|
||||
println "Failed to initialize core, exiting"
|
||||
|
@@ -53,7 +53,7 @@ class CliDownloader {
|
||||
|
||||
Core core
|
||||
try {
|
||||
core = new Core(props, home, "0.1.10")
|
||||
core = new Core(props, home, "0.2.4")
|
||||
} catch (Exception bad) {
|
||||
bad.printStackTrace(System.out)
|
||||
println "Failed to initialize core, exiting"
|
||||
|
@@ -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
|
||||
@@ -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.10")
|
||||
Core core = new Core(props, home, "0.2.4")
|
||||
core.startServices()
|
||||
|
||||
// ... at the end, sleep or execute script
|
||||
|
@@ -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, "")
|
||||
|
@@ -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))
|
||||
}
|
||||
|
||||
|
@@ -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
|
||||
@@ -39,6 +41,8 @@ class ConnectionAcceptor {
|
||||
final ExecutorService acceptorThread
|
||||
final ExecutorService handshakerThreads
|
||||
|
||||
private volatile shutdown
|
||||
|
||||
ConnectionAcceptor(EventBus eventBus, UltrapeerConnectionManager manager,
|
||||
MuWireSettings settings, I2PAcceptor acceptor, HostCache hostCache,
|
||||
TrustService trustService, SearchManager searchManager, UploadManager uploadManager,
|
||||
@@ -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 {
|
||||
|
@@ -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) {}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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()
|
||||
}
|
||||
|
@@ -135,4 +135,9 @@ public class DownloadManager {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
downloaders.each { it.stop() }
|
||||
Downloader.executorService.shutdownNow()
|
||||
}
|
||||
}
|
||||
|
@@ -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,19 +63,10 @@ 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")
|
||||
int piece = pieces.claim()
|
||||
if (piece == -1)
|
||||
return false
|
||||
}
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
claimed.markDownloaded(piece)
|
||||
boolean unclaim = true
|
||||
|
||||
log.info("will download piece $piece")
|
||||
|
||||
@@ -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,6 +125,8 @@ class DownloadSession {
|
||||
}
|
||||
|
||||
// start the download
|
||||
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)
|
||||
@@ -165,12 +157,15 @@ class DownloadSession {
|
||||
System.arraycopy(infoHash.getHashList(), piece * 32, expected, 0, 32)
|
||||
if (hash != expected)
|
||||
throw new BadHashException()
|
||||
|
||||
downloaded.markDownloaded(piece)
|
||||
} finally {
|
||||
claimed.clear(piece)
|
||||
try { channel?.close() } catch (IOException ignore) {}
|
||||
}
|
||||
pieces.markDownloaded(piece)
|
||||
unclaim = false
|
||||
} finally {
|
||||
if (unclaim)
|
||||
pieces.unclaim(piece)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
|
@@ -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() {
|
||||
synchronized(piecesFile) {
|
||||
if (piecesFileClosed)
|
||||
return
|
||||
piecesFile.withPrintWriter { writer ->
|
||||
downloaded.getDownloaded().each { piece ->
|
||||
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) {
|
||||
if (pieces.isComplete() && eventFired.compareAndSet(false, true)) {
|
||||
synchronized(piecesFile) {
|
||||
piecesFileClosed = true
|
||||
piecesFile.delete()
|
||||
eventFired = true
|
||||
}
|
||||
eventBus.publish(
|
||||
new FileDownloadedEvent(
|
||||
downloadedFile : new DownloadedFile(file, getInfoHash(), pieceSizePow2, Collections.emptySet()),
|
||||
|
@@ -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()
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,4 @@
|
||||
package com.muwire.core.files
|
||||
|
||||
class DirectoryWatcher {
|
||||
}
|
@@ -0,0 +1,8 @@
|
||||
package com.muwire.core.search
|
||||
|
||||
import com.muwire.core.Event
|
||||
|
||||
class UIResultBatchEvent extends Event {
|
||||
UUID uuid
|
||||
UIResultEvent[] results
|
||||
}
|
@@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@@ -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();
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
group = com.muwire
|
||||
version = 0.1.10
|
||||
version = 0.2.4
|
||||
groovyVersion = 2.4.15
|
||||
slf4jVersion = 1.7.25
|
||||
spockVersion = 1.1-groovy-2.4
|
||||
|
@@ -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) {
|
||||
|
@@ -93,6 +93,10 @@ class OptionsController {
|
||||
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)
|
||||
|
@@ -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")
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
@@ -70,6 +70,8 @@ class MainFrameModel {
|
||||
|
||||
private long lastRetryTime = System.currentTimeMillis()
|
||||
|
||||
UISettings uiSettings
|
||||
|
||||
void updateTablePreservingSelection(String tableName) {
|
||||
def downloadTable = builder.getVariable(tableName)
|
||||
int selectedRow = downloadTable.getSelectedRow()
|
||||
@@ -79,7 +81,7 @@ class MainFrameModel {
|
||||
|
||||
void mvcGroupInit(Map<String, Object> args) {
|
||||
|
||||
UISettings uiSettings = application.context.get("ui-settings")
|
||||
uiSettings = application.context.get("ui-settings")
|
||||
|
||||
Timer timer = new Timer("download-pumper", true)
|
||||
timer.schedule({
|
||||
@@ -114,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)
|
||||
@@ -160,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
|
||||
@@ -177,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()
|
||||
}
|
||||
@@ -192,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()
|
||||
}
|
||||
@@ -258,14 +268,23 @@ class MainFrameModel {
|
||||
void onQueryEvent(QueryEvent e) {
|
||||
if (e.replyTo == core.me.destination)
|
||||
return
|
||||
|
||||
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(" ")
|
||||
}
|
||||
def search = sb.toString()
|
||||
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)
|
||||
@@ -303,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
|
||||
}
|
||||
}
|
||||
}
|
@@ -27,6 +27,7 @@ class OptionsModel {
|
||||
@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")
|
||||
@@ -48,5 +49,6 @@ class OptionsModel {
|
||||
clearCancelledDownloads = uiSettings.clearCancelledDownloads
|
||||
clearFinishedDownloads = uiSettings.clearFinishedDownloads
|
||||
excludeLocalResult = uiSettings.excludeLocalResult
|
||||
showSearchHashes = uiSettings.showSearchHashes
|
||||
}
|
||||
}
|
@@ -52,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()
|
||||
}
|
||||
}
|
||||
}
|
@@ -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,6 +47,7 @@ class MainFrameView {
|
||||
|
||||
def downloadsTable
|
||||
def lastDownloadSortEvent
|
||||
def lastSharedSortEvent
|
||||
|
||||
void initUI() {
|
||||
UISettings settings = application.context.get("ui-settings")
|
||||
@@ -81,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)
|
||||
}
|
||||
}
|
||||
@@ -117,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 ->
|
||||
@@ -157,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()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -183,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"
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -265,17 +271,23 @@ class MainFrameView {
|
||||
selectionModel.setSelectionMode(ListSelectionModel.SINGLE_SELECTION)
|
||||
selectionModel.addListSelectionListener({
|
||||
int selectedRow = selectedDownloaderRow()
|
||||
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:
|
||||
@@ -289,10 +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() {
|
||||
@@ -302,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")
|
||||
|
@@ -43,6 +43,7 @@ class OptionsView {
|
||||
def clearCancelledDownloadsCheckbox
|
||||
def clearFinishedDownloadsCheckbox
|
||||
def excludeLocalResultCheckbox
|
||||
def showSearchHashesCheckbox
|
||||
|
||||
def buttonsPanel
|
||||
|
||||
@@ -96,6 +97,8 @@ class OptionsView {
|
||||
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()
|
||||
|
@@ -4,10 +4,13 @@ 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
|
||||
@@ -17,6 +20,10 @@ 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
|
||||
|
||||
@@ -95,6 +102,30 @@ class SearchTabView {
|
||||
|
||||
|
||||
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 = {
|
||||
@@ -103,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)
|
||||
}
|
||||
}
|
@@ -8,6 +8,7 @@ class UISettings {
|
||||
boolean clearCancelledDownloads
|
||||
boolean clearFinishedDownloads
|
||||
boolean excludeLocalResult
|
||||
boolean showSearchHashes
|
||||
|
||||
UISettings(Properties props) {
|
||||
lnf = props.getProperty("lnf", "system")
|
||||
@@ -16,6 +17,7 @@ class UISettings {
|
||||
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 {
|
||||
@@ -25,6 +27,7 @@ class UISettings {
|
||||
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)
|
||||
|
||||
|
Reference in New Issue
Block a user