Compare commits
62 Commits
muwire-0.1
...
file-monit
Author | SHA1 | Date | |
---|---|---|---|
![]() |
6a40787863 | ||
![]() |
c698cbd737 | ||
![]() |
9c049b9301 | ||
![]() |
84a9bb9482 | ||
![]() |
c46f1b1ccd | ||
![]() |
7e2c4d48c6 | ||
![]() |
71a919e62b | ||
![]() |
d5eb65bdc2 | ||
![]() |
aef7533bd5 | ||
![]() |
e78016ead4 | ||
![]() |
52ced669dd | ||
![]() |
b52fb38ede | ||
![]() |
5dcef3ca05 | ||
![]() |
eaa0e46ce5 | ||
![]() |
c4f48c02b6 | ||
![]() |
5c16335969 | ||
![]() |
546eb4e9d3 | ||
![]() |
c3d9e852ba | ||
![]() |
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 |
@@ -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)
|
@@ -4,6 +4,7 @@ import java.util.concurrent.CountDownLatch
|
||||
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.MuWireSettings
|
||||
import com.muwire.core.UILoadedEvent
|
||||
import com.muwire.core.connection.ConnectionAttemptStatus
|
||||
import com.muwire.core.connection.ConnectionEvent
|
||||
import com.muwire.core.connection.DisconnectionEvent
|
||||
@@ -34,7 +35,7 @@ class Cli {
|
||||
|
||||
Core core
|
||||
try {
|
||||
core = new Core(props, home, "0.1.11")
|
||||
core = new Core(props, home, "0.2.5")
|
||||
} catch (Exception bad) {
|
||||
bad.printStackTrace(System.out)
|
||||
println "Failed to initialize core, exiting"
|
||||
@@ -84,6 +85,7 @@ class Cli {
|
||||
core.eventBus.register(AllFilesLoadedEvent.class, fileLoader)
|
||||
core.startServices()
|
||||
|
||||
core.eventBus.publish(new UILoadedEvent())
|
||||
println "waiting for files to load"
|
||||
latch.await()
|
||||
// now we begin
|
||||
|
@@ -53,7 +53,7 @@ class CliDownloader {
|
||||
|
||||
Core core
|
||||
try {
|
||||
core = new Core(props, home, "0.1.11")
|
||||
core = new Core(props, home, "0.2.5")
|
||||
} catch (Exception bad) {
|
||||
bad.printStackTrace(System.out)
|
||||
println "Failed to initialize core, exiting"
|
||||
|
@@ -23,6 +23,7 @@ import com.muwire.core.files.FileSharedEvent
|
||||
import com.muwire.core.files.FileUnsharedEvent
|
||||
import com.muwire.core.files.HasherService
|
||||
import com.muwire.core.files.PersisterService
|
||||
import com.muwire.core.files.DirectoryWatcher
|
||||
import com.muwire.core.hostcache.CacheClient
|
||||
import com.muwire.core.hostcache.HostCache
|
||||
import com.muwire.core.hostcache.HostDiscoveredEvent
|
||||
@@ -70,6 +71,7 @@ public class Core {
|
||||
private final ConnectionEstablisher connectionEstablisher
|
||||
private final HasherService hasherService
|
||||
private final DownloadManager downloadManager
|
||||
private final DirectoryWatcher directoryWatcher
|
||||
|
||||
public Core(MuWireSettings props, File home, String myVersion) {
|
||||
this.home = home
|
||||
@@ -162,6 +164,7 @@ public class Core {
|
||||
|
||||
log.info "initializing persistence service"
|
||||
persisterService = new PersisterService(new File(home, "files.json"), eventBus, 15000, fileManager)
|
||||
eventBus.register(UILoadedEvent.class, persisterService)
|
||||
|
||||
log.info("initializing host cache")
|
||||
File hostStorage = new File(home, "hosts.json")
|
||||
@@ -213,6 +216,9 @@ public class Core {
|
||||
connectionAcceptor = new ConnectionAcceptor(eventBus, connectionManager, props,
|
||||
i2pAcceptor, hostCache, trustService, searchManager, uploadManager, connectionEstablisher)
|
||||
|
||||
log.info("initializing directory watcher")
|
||||
directoryWatcher = new DirectoryWatcher(eventBus, fileManager)
|
||||
eventBus.register(FileSharedEvent.class, directoryWatcher)
|
||||
|
||||
log.info("initializing hasher service")
|
||||
hasherService = new HasherService(new FileHasher(), eventBus, fileManager)
|
||||
@@ -221,9 +227,9 @@ public class Core {
|
||||
|
||||
public void startServices() {
|
||||
hasherService.start()
|
||||
directoryWatcher.start()
|
||||
trustService.start()
|
||||
trustService.waitForLoad()
|
||||
persisterService.start()
|
||||
hostCache.start()
|
||||
connectionManager.start()
|
||||
cacheClient.start()
|
||||
@@ -234,14 +240,16 @@ public class Core {
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
log.info("shutting down connection manager")
|
||||
connectionManager.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 directory watcher")
|
||||
directoryWatcher.stop()
|
||||
log.info("shutting down connection manager")
|
||||
connectionManager.shutdown()
|
||||
}
|
||||
|
||||
static main(args) {
|
||||
@@ -268,7 +276,7 @@ public class Core {
|
||||
}
|
||||
}
|
||||
|
||||
Core core = new Core(props, home, "0.1.11")
|
||||
Core core = new Core(props, home, "0.2.5")
|
||||
core.startServices()
|
||||
|
||||
// ... at the end, sleep or execute script
|
||||
|
@@ -1,6 +1,11 @@
|
||||
package com.muwire.core
|
||||
|
||||
import java.util.stream.Collectors
|
||||
|
||||
import com.muwire.core.hostcache.CrawlerResponse
|
||||
import com.muwire.core.util.DataUtil
|
||||
|
||||
import net.i2p.data.Base64
|
||||
|
||||
class MuWireSettings {
|
||||
|
||||
@@ -10,9 +15,9 @@ class MuWireSettings {
|
||||
int updateCheckInterval
|
||||
String nickname
|
||||
File downloadLocation
|
||||
String sharedFiles
|
||||
CrawlerResponse crawlerResponse
|
||||
boolean shareDownloadedFiles
|
||||
Set<String> watchedDirectories
|
||||
|
||||
MuWireSettings() {
|
||||
this(new Properties())
|
||||
@@ -25,10 +30,16 @@ class MuWireSettings {
|
||||
nickname = props.getProperty("nickname","MuWireUser")
|
||||
downloadLocation = new File((String)props.getProperty("downloadLocation",
|
||||
System.getProperty("user.home")))
|
||||
sharedFiles = props.getProperty("sharedFiles")
|
||||
downloadRetryInterval = Integer.parseInt(props.getProperty("downloadRetryInterval","15"))
|
||||
updateCheckInterval = Integer.parseInt(props.getProperty("updateCheckInterval","36"))
|
||||
shareDownloadedFiles = Boolean.parseBoolean(props.getProperty("shareDownloadedFiles","true"))
|
||||
|
||||
watchedDirectories = new HashSet<>()
|
||||
if (props.containsKey("watchedDirectories")) {
|
||||
String[] encoded = props.getProperty("watchedDirectories").split(",")
|
||||
encoded.each { watchedDirectories << DataUtil.readi18nString(Base64.decode(it)) }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void write(OutputStream out) throws IOException {
|
||||
@@ -41,8 +52,14 @@ class MuWireSettings {
|
||||
props.setProperty("downloadRetryInterval", String.valueOf(downloadRetryInterval))
|
||||
props.setProperty("updateCheckInterval", String.valueOf(updateCheckInterval))
|
||||
props.setProperty("shareDownloadedFiles", String.valueOf(shareDownloadedFiles))
|
||||
if (sharedFiles != null)
|
||||
props.setProperty("sharedFiles", sharedFiles)
|
||||
|
||||
if (!watchedDirectories.isEmpty()) {
|
||||
String encoded = watchedDirectories.stream().
|
||||
map({Base64.encode(DataUtil.encodei18nString(it))}).
|
||||
collect(Collectors.joining(","))
|
||||
props.setProperty("watchedDirectories", encoded)
|
||||
}
|
||||
|
||||
props.store(out, "")
|
||||
}
|
||||
|
||||
|
@@ -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
|
||||
@@ -225,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) {
|
||||
|
@@ -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
|
||||
}
|
||||
|
||||
|
@@ -4,9 +4,13 @@ import com.muwire.core.InfoHash
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.connection.Endpoint
|
||||
|
||||
import java.nio.file.AtomicMoveNotSupportedException
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.StandardCopyOption
|
||||
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 +38,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
|
||||
@@ -42,12 +46,14 @@ public class Downloader {
|
||||
private final Set<Destination> destinations
|
||||
private final int nPieces
|
||||
private final File piecesFile
|
||||
private final File incompleteFile
|
||||
final int pieceSizePow2
|
||||
private final Map<Destination, DownloadWorker> activeWorkers = new ConcurrentHashMap<>()
|
||||
|
||||
|
||||
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,
|
||||
@@ -62,6 +68,7 @@ public class Downloader {
|
||||
this.connector = connector
|
||||
this.destinations = destinations
|
||||
this.piecesFile = new File(incompletes, file.getName()+".pieces")
|
||||
this.incompleteFile = new File(incompletes, file.getName()+".part")
|
||||
this.pieceSizePow2 = pieceSizePow2
|
||||
this.pieceSize = 1 << pieceSizePow2
|
||||
|
||||
@@ -72,8 +79,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 +106,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 +146,7 @@ public class Downloader {
|
||||
allFinished &= it.currentState == WorkerState.FINISHED
|
||||
}
|
||||
if (allFinished) {
|
||||
if (downloaded.isComplete())
|
||||
if (pieces.isComplete())
|
||||
return DownloadState.FINISHED
|
||||
return DownloadState.FAILED
|
||||
}
|
||||
@@ -170,9 +180,12 @@ public class Downloader {
|
||||
public void cancel() {
|
||||
cancelled = true
|
||||
stop()
|
||||
file.delete()
|
||||
synchronized(piecesFile) {
|
||||
piecesFileClosed = true
|
||||
piecesFile.delete()
|
||||
}
|
||||
incompleteFile.delete()
|
||||
}
|
||||
|
||||
void stop() {
|
||||
activeWorkers.values().each {
|
||||
@@ -231,8 +244,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, incompleteFile, pieceSize, length)
|
||||
requestPerformed = currentSession.request()
|
||||
if (!requestPerformed)
|
||||
break
|
||||
@@ -242,9 +255,17 @@ 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
|
||||
}
|
||||
try {
|
||||
Files.move(incompleteFile.toPath(), file.toPath(), StandardCopyOption.ATOMIC_MOVE)
|
||||
} catch (AtomicMoveNotSupportedException e) {
|
||||
Files.copy(incompleteFile.toPath(), file.toPath(), StandardCopyOption.REPLACE_EXISTING)
|
||||
incompleteFile.delete()
|
||||
}
|
||||
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,137 @@
|
||||
package com.muwire.core.files
|
||||
|
||||
import java.nio.file.FileSystem
|
||||
import java.nio.file.FileSystems
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import static java.nio.file.StandardWatchEventKinds.*
|
||||
import java.nio.file.WatchEvent
|
||||
import java.nio.file.WatchKey
|
||||
import java.nio.file.WatchService
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
import com.muwire.core.EventBus
|
||||
import com.muwire.core.SharedFile
|
||||
|
||||
import groovy.util.logging.Log
|
||||
import net.i2p.util.SystemVersion
|
||||
|
||||
@Log
|
||||
class DirectoryWatcher {
|
||||
|
||||
private static final long WAIT_TIME = 1000
|
||||
|
||||
private static final WatchEvent.Kind[] kinds
|
||||
static {
|
||||
if (SystemVersion.isMac())
|
||||
kinds = [ENTRY_MODIFY, ENTRY_DELETE]
|
||||
else
|
||||
kinds = [ENTRY_CREATE, ENTRY_MODIFY, ENTRY_DELETE]
|
||||
}
|
||||
|
||||
private final EventBus eventBus
|
||||
private final FileManager fileManager
|
||||
private final Thread watcherThread, publisherThread
|
||||
private final Map<File, Long> waitingFiles = new ConcurrentHashMap<>()
|
||||
private WatchService watchService
|
||||
private volatile boolean shutdown
|
||||
|
||||
DirectoryWatcher(EventBus eventBus, FileManager fileManager) {
|
||||
this.eventBus = eventBus
|
||||
this.fileManager = fileManager
|
||||
this.watcherThread = new Thread({watch() } as Runnable, "directory-watcher")
|
||||
watcherThread.setDaemon(true)
|
||||
this.publisherThread = new Thread({publish()} as Runnable, "watched-files-publisher")
|
||||
publisherThread.setDaemon(true)
|
||||
}
|
||||
|
||||
void start() {
|
||||
watchService = FileSystems.getDefault().newWatchService()
|
||||
watcherThread.start()
|
||||
publisherThread.start()
|
||||
}
|
||||
|
||||
void stop() {
|
||||
shutdown = true
|
||||
watcherThread.interrupt()
|
||||
publisherThread.interrupt()
|
||||
watchService.close()
|
||||
}
|
||||
|
||||
void onFileSharedEvent(FileSharedEvent e) {
|
||||
if (!e.file.isDirectory())
|
||||
return
|
||||
Path path = e.file.getCanonicalFile().toPath()
|
||||
path.register(watchService, kinds)
|
||||
|
||||
}
|
||||
|
||||
private void watch() {
|
||||
try {
|
||||
while(!shutdown) {
|
||||
WatchKey key = watchService.take()
|
||||
key.pollEvents().each {
|
||||
switch(it.kind()) {
|
||||
case ENTRY_CREATE: processCreated(key.watchable(), it.context()); break
|
||||
case ENTRY_MODIFY: processModified(key.watchable(), it.context()); break
|
||||
case ENTRY_DELETE: processDeleted(key.watchable(), it.context()); break
|
||||
}
|
||||
}
|
||||
key.reset()
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
if (!shutdown)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void processCreated(Path parent, Path path) {
|
||||
File f= join(parent, path)
|
||||
log.fine("created entry $f")
|
||||
if (f.isDirectory())
|
||||
f.toPath().register(watchService, kinds)
|
||||
}
|
||||
|
||||
private void processModified(Path parent, Path path) {
|
||||
File f = join(parent, path)
|
||||
log.fine("modified entry $f")
|
||||
waitingFiles.put(f, System.currentTimeMillis())
|
||||
}
|
||||
|
||||
private void processDeleted(Path parent, Path path) {
|
||||
File f = join(parent, path)
|
||||
log.fine("deleted entry $f")
|
||||
SharedFile sf = fileManager.fileToSharedFile.get(f)
|
||||
if (sf != null)
|
||||
eventBus.publish(new FileUnsharedEvent(unsharedFile : sf))
|
||||
}
|
||||
|
||||
private static File join(Path parent, Path path) {
|
||||
File parentFile = parent.toFile().getCanonicalFile()
|
||||
new File(parentFile, path.toFile().getName())
|
||||
}
|
||||
|
||||
private void publish() {
|
||||
try {
|
||||
while(!shutdown) {
|
||||
Thread.sleep(WAIT_TIME)
|
||||
long now = System.currentTimeMillis()
|
||||
def published = []
|
||||
waitingFiles.each { file, timestamp ->
|
||||
if (now - timestamp > WAIT_TIME) {
|
||||
log.fine("publishing file $file")
|
||||
eventBus.publish new FileSharedEvent(file : file)
|
||||
published << file
|
||||
}
|
||||
}
|
||||
published.each {
|
||||
waitingFiles.remove(it)
|
||||
}
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
if (!shutdown)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
@@ -32,7 +32,7 @@ class HasherService {
|
||||
private void process(File f) {
|
||||
f = f.getCanonicalFile()
|
||||
if (f.isDirectory()) {
|
||||
f.listFiles().each {onFileSharedEvent new FileSharedEvent(file: it) }
|
||||
f.listFiles().each {eventBus.publish new FileSharedEvent(file: it) }
|
||||
} else {
|
||||
if (f.length() == 0) {
|
||||
eventBus.publish new FileHashedEvent(error: "Not sharing empty file $f")
|
||||
|
@@ -11,6 +11,7 @@ import com.muwire.core.EventBus
|
||||
import com.muwire.core.InfoHash
|
||||
import com.muwire.core.Service
|
||||
import com.muwire.core.SharedFile
|
||||
import com.muwire.core.UILoadedEvent
|
||||
import com.muwire.core.util.DataUtil
|
||||
|
||||
import groovy.json.JsonOutput
|
||||
@@ -36,14 +37,14 @@ class PersisterService extends Service {
|
||||
timer = new Timer("file persister", true)
|
||||
}
|
||||
|
||||
void start() {
|
||||
timer.schedule({load()} as TimerTask, 1)
|
||||
}
|
||||
|
||||
void stop() {
|
||||
timer.cancel()
|
||||
}
|
||||
|
||||
void onUILoadedEvent(UILoadedEvent e) {
|
||||
timer.schedule({load()} as TimerTask, 1)
|
||||
}
|
||||
|
||||
void load() {
|
||||
if (location.exists() && location.isFile()) {
|
||||
def slurper = new JsonSlurper()
|
||||
|
@@ -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.11
|
||||
version = 0.2.5
|
||||
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
|
||||
}
|
||||
|
||||
@@ -103,7 +118,10 @@ class MainFrameController {
|
||||
void download() {
|
||||
def result = selectedResult()
|
||||
if (result == null)
|
||||
return // TODO disable button
|
||||
return
|
||||
|
||||
if (!model.canDownload(result.infohash))
|
||||
return
|
||||
|
||||
def file = new File(application.context.get("muwire-settings").downloadLocation, result.name)
|
||||
|
||||
@@ -135,6 +153,7 @@ class MainFrameController {
|
||||
void cancel() {
|
||||
def downloader = model.downloads[selectedDownload()].downloader
|
||||
downloader.cancel()
|
||||
model.downloadInfoHashes.remove(downloader.getInfoHash())
|
||||
core.eventBus.publish(new UIDownloadCancelledEvent(downloader : downloader))
|
||||
}
|
||||
|
||||
@@ -171,20 +190,17 @@ 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 saveMuWireSettings() {
|
||||
File f = new File(core.home, "MuWire.properties")
|
||||
f.withOutputStream {
|
||||
core.muOptions.write(it)
|
||||
}
|
||||
}
|
||||
|
||||
void mvcGroupInit(Map<String, String> args) {
|
||||
application.addPropertyChangeListener("core", {e->
|
||||
core = e.getNewValue()
|
||||
|
@@ -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)
|
||||
|
@@ -1,3 +1,4 @@
|
||||
|
||||
import griffon.core.GriffonApplication
|
||||
import griffon.core.env.Metadata
|
||||
import groovy.util.logging.Log
|
||||
@@ -104,12 +105,6 @@ class Ready extends AbstractLifecycleHandler {
|
||||
it.propertyChange(new PropertyChangeEvent(this, "core", null, core))
|
||||
}
|
||||
|
||||
if (props.sharedFiles != null) {
|
||||
props.sharedFiles.split(",").each {
|
||||
core.eventBus.publish(new FileSharedEvent(file : new File(it)))
|
||||
}
|
||||
}
|
||||
|
||||
core.eventBus.publish(new UILoadedEvent())
|
||||
}
|
||||
}
|
||||
|
@@ -9,6 +9,7 @@ import javax.swing.JTable
|
||||
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.InfoHash
|
||||
import com.muwire.core.MuWireSettings
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.connection.ConnectionAttemptStatus
|
||||
import com.muwire.core.connection.ConnectionEvent
|
||||
@@ -19,7 +20,9 @@ import com.muwire.core.files.FileDownloadedEvent
|
||||
import com.muwire.core.files.FileHashedEvent
|
||||
import com.muwire.core.files.FileLoadedEvent
|
||||
import com.muwire.core.files.FileSharedEvent
|
||||
import com.muwire.core.files.FileUnsharedEvent
|
||||
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 +37,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
|
||||
|
||||
@@ -51,25 +55,29 @@ class MainFrameModel {
|
||||
def downloads = []
|
||||
def uploads = []
|
||||
def shared = []
|
||||
def watched = []
|
||||
def connectionList = []
|
||||
def searches = new LinkedList()
|
||||
def trusted = []
|
||||
def distrusted = []
|
||||
|
||||
boolean hashSearch
|
||||
|
||||
@Observable int connections
|
||||
@Observable String me
|
||||
@Observable boolean searchButtonsEnabled
|
||||
@Observable boolean downloadActionEnabled
|
||||
@Observable boolean trustButtonsEnabled
|
||||
@Observable boolean cancelButtonEnabled
|
||||
@Observable boolean retryButtonEnabled
|
||||
|
||||
private final Set<InfoHash> infoHashes = new HashSet<>()
|
||||
|
||||
private final Set<InfoHash> downloadInfoHashes = new HashSet<>()
|
||||
|
||||
volatile Core core
|
||||
|
||||
private long lastRetryTime = System.currentTimeMillis()
|
||||
|
||||
UISettings uiSettings
|
||||
|
||||
void updateTablePreservingSelection(String tableName) {
|
||||
def downloadTable = builder.getVariable(tableName)
|
||||
int selectedRow = downloadTable.getSelectedRow()
|
||||
@@ -79,7 +87,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 +122,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)
|
||||
@@ -125,9 +134,10 @@ class MainFrameModel {
|
||||
core.eventBus.register(QueryEvent.class, this)
|
||||
core.eventBus.register(UpdateAvailableEvent.class, this)
|
||||
core.eventBus.register(FileDownloadedEvent.class, this)
|
||||
core.eventBus.register(FileUnsharedEvent.class, this)
|
||||
|
||||
timer.schedule({
|
||||
int retryInterval = application.context.get("muwire-settings").downloadRetryInterval
|
||||
int retryInterval = core.muOptions.downloadRetryInterval
|
||||
if (retryInterval > 0) {
|
||||
retryInterval *= 60000
|
||||
long now = System.currentTimeMillis()
|
||||
@@ -150,6 +160,10 @@ class MainFrameModel {
|
||||
runInsideUIAsync {
|
||||
trusted.addAll(core.trustService.good.values())
|
||||
distrusted.addAll(core.trustService.bad.values())
|
||||
|
||||
watched.addAll(core.muOptions.watchedDirectories)
|
||||
builder.getVariable("watched-directories-table").model.fireTableDataChanged()
|
||||
watched.each { core.eventBus.publish(new FileSharedEvent(file : new File(it))) }
|
||||
}
|
||||
})
|
||||
|
||||
@@ -160,9 +174,15 @@ 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
|
||||
downloadInfoHashes.add(e.downloader.infoHash)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,7 +197,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 +213,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()
|
||||
}
|
||||
@@ -222,6 +244,17 @@ class MainFrameModel {
|
||||
}
|
||||
}
|
||||
|
||||
void onFileUnsharedEvent(FileUnsharedEvent e) {
|
||||
InfoHash infohash = e.unsharedFile.infoHash
|
||||
if (!infoHashes.remove(infohash))
|
||||
return
|
||||
runInsideUIAsync {
|
||||
shared.remove(e.unsharedFile)
|
||||
JTable table = builder.getVariable("shared-files-table")
|
||||
table.model.fireTableDataChanged()
|
||||
}
|
||||
}
|
||||
|
||||
void onUploadEvent(UploadEvent e) {
|
||||
runInsideUIAsync {
|
||||
uploads << e.uploader
|
||||
@@ -258,14 +291,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 +345,26 @@ 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
|
||||
}
|
||||
}
|
||||
|
||||
boolean canDownload(InfoHash hash) {
|
||||
!downloadInfoHashes.contains(hash)
|
||||
}
|
||||
}
|
@@ -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
|
||||
@@ -13,6 +14,7 @@ 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
|
||||
@@ -28,6 +30,8 @@ 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
|
||||
@@ -43,6 +47,7 @@ class MainFrameView {
|
||||
|
||||
def downloadsTable
|
||||
def lastDownloadSortEvent
|
||||
def lastSharedSortEvent
|
||||
|
||||
void initUI() {
|
||||
UISettings settings = application.context.get("ui-settings")
|
||||
@@ -85,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)
|
||||
}
|
||||
}
|
||||
@@ -106,9 +105,9 @@ class MainFrameView {
|
||||
borderLayout()
|
||||
tabbedPane(id : "result-tabs", constraints: BorderLayout.CENTER)
|
||||
panel(constraints : BorderLayout.SOUTH) {
|
||||
button(text : "Download", enabled : bind {model.searchButtonsEnabled}, downloadAction)
|
||||
button(text : "Trust", enabled: bind {model.searchButtonsEnabled }, trustAction)
|
||||
button(text : "Distrust", enabled : bind {model.searchButtonsEnabled}, distrustAction)
|
||||
button(text : "Download", enabled : bind {model.downloadActionEnabled}, downloadAction)
|
||||
button(text : "Trust", enabled: bind {model.trustButtonsEnabled }, trustAction)
|
||||
button(text : "Distrust", enabled : bind {model.trustButtonsEnabled}, distrustAction)
|
||||
}
|
||||
}
|
||||
panel (constraints : JSplitPane.BOTTOM) {
|
||||
@@ -121,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 ->
|
||||
@@ -139,16 +138,32 @@ class MainFrameView {
|
||||
}
|
||||
panel (constraints: "uploads window"){
|
||||
gridLayout(cols : 1, rows : 2)
|
||||
panel {
|
||||
gridLayout(cols : 2, rows : 1)
|
||||
panel {
|
||||
borderLayout()
|
||||
panel (constraints : BorderLayout.NORTH) {
|
||||
button(text : "Click here to share files", actionPerformed : shareFiles)
|
||||
button(text : "Add directories to watch", actionPerformed : watchDirectories)
|
||||
}
|
||||
scrollPane (constraints : BorderLayout.CENTER) {
|
||||
table(id : "watched-directories-table", autoCreateRowSorter: true) {
|
||||
tableModel(list : model.watched) {
|
||||
closureColumn(header: "Watched Directories", type : String, read : { it })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
panel {
|
||||
borderLayout()
|
||||
panel (constraints : BorderLayout.NORTH) {
|
||||
button(text : "Share files", actionPerformed : shareFiles)
|
||||
}
|
||||
scrollPane(constraints : BorderLayout.CENTER) {
|
||||
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 : Long, read : {row -> row.file.length() })
|
||||
closureColumn(header : "Name", preferredWidth : 500, type : String, read : {row -> row.file.getAbsolutePath()})
|
||||
closureColumn(header : "Size", preferredWidth : 100, type : Long, read : {row -> row.file.length() })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -161,16 +176,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()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -187,7 +199,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"
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -280,11 +298,12 @@ class MainFrameView {
|
||||
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:
|
||||
@@ -298,15 +317,34 @@ 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()})
|
||||
sharedFilesMenu.add(unshareSelectedFiles)
|
||||
// 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) {
|
||||
@@ -319,12 +357,54 @@ class MainFrameView {
|
||||
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)
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
def showPopupMenu(JPopupMenu menu, MouseEvent event) {
|
||||
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() {
|
||||
int selected = builder.getVariable("downloads-table").getSelectedRow()
|
||||
if (lastDownloadSortEvent != null)
|
||||
@@ -332,6 +412,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")
|
||||
@@ -354,11 +482,26 @@ class MainFrameView {
|
||||
|
||||
def shareFiles = {
|
||||
def chooser = new JFileChooser()
|
||||
chooser.setDialogTitle("Select file or directory to share")
|
||||
chooser.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES)
|
||||
chooser.setDialogTitle("Select file to share")
|
||||
chooser.setFileSelectionMode(JFileChooser.FILES_ONLY)
|
||||
int rv = chooser.showOpenDialog(null)
|
||||
if (rv == JFileChooser.APPROVE_OPTION) {
|
||||
model.core.eventBus.publish(new FileSharedEvent(file : chooser.getSelectedFile()))
|
||||
}
|
||||
}
|
||||
|
||||
def watchDirectories = {
|
||||
def chooser = new JFileChooser()
|
||||
chooser.setDialogTitle("Select directory to watch")
|
||||
chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY)
|
||||
int rv = chooser.showOpenDialog(null)
|
||||
if (rv == JFileChooser.APPROVE_OPTION) {
|
||||
File f = chooser.getSelectedFile()
|
||||
model.watched << f.getAbsolutePath()
|
||||
application.context.get("muwire-settings").watchedDirectories << f.getAbsolutePath()
|
||||
mvcGroup.controller.saveMuWireSettings()
|
||||
builder.getVariable("watched-directories-table").model.fireTableDataChanged()
|
||||
model.core.eventBus.publish(new FileSharedEvent(file : f))
|
||||
}
|
||||
}
|
||||
}
|
@@ -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,8 @@ 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
|
||||
|
||||
@@ -61,7 +66,13 @@ class SearchTabView {
|
||||
def selectionModel = resultsTable.getSelectionModel()
|
||||
selectionModel.setSelectionMode(ListSelectionModel.SINGLE_SELECTION)
|
||||
selectionModel.addListSelectionListener( {
|
||||
mvcGroup.parentGroup.model.searchButtonsEnabled = true
|
||||
int row = resultsTable.getSelectedRow()
|
||||
if (row < 0)
|
||||
return
|
||||
if (lastSortEvent != null)
|
||||
row = resultsTable.rowSorter.convertRowIndexToModel(row)
|
||||
mvcGroup.parentGroup.model.trustButtonsEnabled = true
|
||||
mvcGroup.parentGroup.model.downloadActionEnabled = mvcGroup.parentGroup.model.canDownload(model.results[row].infohash)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -97,20 +108,55 @@ class SearchTabView {
|
||||
|
||||
|
||||
resultsTable.rowSorter.addRowSorterListener({ evt -> lastSortEvent = evt})
|
||||
resultsTable.rowSorter.setSortsOnUpdates(true)
|
||||
|
||||
|
||||
resultsTable.addMouseListener(new MouseAdapter() {
|
||||
@Override
|
||||
public void mouseClicked(MouseEvent e) {
|
||||
if (e.button == MouseEvent.BUTTON1 && e.clickCount == 2)
|
||||
if (e.button == MouseEvent.BUTTON3)
|
||||
showPopupMenu(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(e)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
def closeTab = {
|
||||
int index = parent.indexOfTab(searchTerms)
|
||||
parent.removeTabAt(index)
|
||||
mvcGroup.parentGroup.model.searchButtonsEnabled = false
|
||||
mvcGroup.parentGroup.model.trustButtonsEnabled = false
|
||||
mvcGroup.parentGroup.model.downloadActionEnabled = false
|
||||
mvcGroup.destroy()
|
||||
}
|
||||
|
||||
def showPopupMenu(MouseEvent e) {
|
||||
JPopupMenu menu = new JPopupMenu()
|
||||
if (mvcGroup.parentGroup.model.downloadActionEnabled) {
|
||||
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)
|
||||
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