Compare commits
202 Commits
muwire-0.4
...
browse-hos
Author | SHA1 | Date | |
---|---|---|---|
![]() |
2b04374e23 | ||
![]() |
383addbc37 | ||
![]() |
cc39cd7f8e | ||
![]() |
83665d7524 | ||
![]() |
94340480b4 | ||
![]() |
8850d49c63 | ||
![]() |
f0f9d840f0 | ||
![]() |
7f4cd4f331 | ||
![]() |
e6162503f6 | ||
![]() |
7a5d71dc36 | ||
![]() |
6fa39a5e35 | ||
![]() |
c5ae804f61 | ||
![]() |
d7695b448d | ||
![]() |
946d9c8f32 | ||
![]() |
02441ca1e3 | ||
![]() |
5fa21b2360 | ||
![]() |
d4c08f4fe6 | ||
![]() |
942de287c6 | ||
![]() |
d0299f80c6 | ||
![]() |
1227cf9263 | ||
![]() |
a05575485f | ||
![]() |
f5bccd8126 | ||
![]() |
70fb789abf | ||
![]() |
feb712c253 | ||
![]() |
d22b403e2a | ||
![]() |
a24982e0df | ||
![]() |
6c26019164 | ||
![]() |
965fa79bbf | ||
![]() |
60ddb85461 | ||
![]() |
c7284623bc | ||
![]() |
3e7f2aa70a | ||
![]() |
4f436a636c | ||
![]() |
b49dbc30c3 | ||
![]() |
c25d314e1c | ||
![]() |
b28587a275 | ||
![]() |
8b8e5d59be | ||
![]() |
70bbe1f636 | ||
![]() |
337605dc0f | ||
![]() |
14bdfa6b2e | ||
![]() |
ed3f9da773 | ||
![]() |
251080d08f | ||
![]() |
f530ab999d | ||
![]() |
4133384e48 | ||
![]() |
600fc98868 | ||
![]() |
129eeb3b88 | ||
![]() |
20b51b78a0 | ||
![]() |
33fe755b60 | ||
![]() |
8b0668a134 | ||
![]() |
730d2202fd | ||
![]() |
69906a986d | ||
![]() |
5bc8fa8633 | ||
![]() |
7de7c9d8f3 | ||
![]() |
e943f6019d | ||
![]() |
2eec7bec5b | ||
![]() |
c36110cf76 | ||
![]() |
abe28517bc | ||
![]() |
15bc4c064d | ||
![]() |
91d771944b | ||
![]() |
e09c456a13 | ||
![]() |
d9c1067226 | ||
![]() |
eda3e7ad3a | ||
![]() |
e9798c7eaa | ||
![]() |
66bb4eef5b | ||
![]() |
55f260b3f4 | ||
![]() |
32d4c3965e | ||
![]() |
de1534d837 | ||
![]() |
7b58e8a88a | ||
![]() |
8a03b89985 | ||
![]() |
1d97374857 | ||
![]() |
549e8c2d98 | ||
![]() |
b54d24db0d | ||
![]() |
fa12e84345 | ||
![]() |
6430ff2691 | ||
![]() |
591313c81c | ||
![]() |
ce7b6a0c65 | ||
![]() |
5c4d4c4580 | ||
![]() |
4cb864ff9f | ||
![]() |
417675ad07 | ||
![]() |
9513e5ba3c | ||
![]() |
85610cf169 | ||
![]() |
e8322384b8 | ||
![]() |
179279ed30 | ||
![]() |
ae79f0fded | ||
![]() |
ed878b3762 | ||
![]() |
623cca0ef2 | ||
![]() |
eaa883c3ba | ||
![]() |
7ae8076865 | ||
![]() |
b1aa92661c | ||
![]() |
9ed94c8376 | ||
![]() |
fa6aea1abe | ||
![]() |
0de84e704b | ||
![]() |
a767dda044 | ||
![]() |
56e9235d7b | ||
![]() |
2fba9a74ce | ||
![]() |
2bb6826906 | ||
![]() |
9f339629a9 | ||
![]() |
58d4207f94 | ||
![]() |
32577a28dc | ||
![]() |
f7b43304d4 | ||
![]() |
dcbe09886d | ||
![]() |
5a54b2dcda | ||
![]() |
581293b24f | ||
![]() |
cd072b9f76 | ||
![]() |
6b74fc5956 | ||
![]() |
3de2f872bb | ||
![]() |
fcde917d08 | ||
![]() |
4ded065010 | ||
![]() |
18a1c7091a | ||
![]() |
46aee19f80 | ||
![]() |
92dd7064c6 | ||
![]() |
b2e4dda677 | ||
![]() |
e77a2c8961 | ||
![]() |
ee2fd2ef68 | ||
![]() |
3f95d2bf1d | ||
![]() |
1390983732 | ||
![]() |
ce660cefe9 | ||
![]() |
72b81eb886 | ||
![]() |
57d593a68a | ||
![]() |
39a81a3376 | ||
![]() |
fd0bf17c24 | ||
![]() |
ac12bff69b | ||
![]() |
feef773bac | ||
![]() |
239d8f12a7 | ||
![]() |
8bbc61a7cb | ||
![]() |
7f31c4477f | ||
![]() |
6bad67c1bf | ||
![]() |
c76e6dc99f | ||
![]() |
acf9db0db3 | ||
![]() |
69b4f0b547 | ||
![]() |
80e165b505 | ||
![]() |
bcce55b873 | ||
![]() |
d5c92560db | ||
![]() |
f827c1c9bf | ||
![]() |
88c5f1a02d | ||
![]() |
d8e44f5f39 | ||
![]() |
72ff47ffe5 | ||
![]() |
066ee2c96d | ||
![]() |
0a8016dea7 | ||
![]() |
db36367b11 | ||
![]() |
b6c9ccb7f6 | ||
![]() |
a9dc636bce | ||
![]() |
3cc0574d11 | ||
![]() |
20fab9b16d | ||
![]() |
4015818323 | ||
![]() |
f569d45c8c | ||
![]() |
3773647869 | ||
![]() |
29cdbf018c | ||
![]() |
94bb7022eb | ||
![]() |
39808302df | ||
![]() |
2d22f9c39e | ||
![]() |
ee8f80bab6 | ||
![]() |
3e6242e583 | ||
![]() |
41181616ee | ||
![]() |
eb2530ca32 | ||
![]() |
b5233780ef | ||
![]() |
78753d7538 | ||
![]() |
4740e8b4f5 | ||
![]() |
ad5b00fc90 | ||
![]() |
d6c6880848 | ||
![]() |
4f948c1b9e | ||
![]() |
2b68c24f9c | ||
![]() |
bcdf0422db | ||
![]() |
f6434b478d | ||
![]() |
e979fdd26f | ||
![]() |
e6bfcaaab9 | ||
![]() |
9780108e8a | ||
![]() |
697c7d2d6d | ||
![]() |
887d10c8bf | ||
![]() |
ef6b8fe458 | ||
![]() |
20ab55d763 | ||
![]() |
eda58c9e0d | ||
![]() |
fb42fc0e35 | ||
![]() |
35cabc47ad | ||
![]() |
5be97d0404 | ||
![]() |
82b0fa253c | ||
![]() |
011a4d5766 | ||
![]() |
5cd1ca88c1 | ||
![]() |
44c880d911 | ||
![]() |
14857cb5ad | ||
![]() |
7daf981f1a | ||
![]() |
b99bc0ea32 | ||
![]() |
1ccf6fbdfa | ||
![]() |
5711979272 | ||
![]() |
9a5e2b1fa3 | ||
![]() |
cafc5f582e | ||
![]() |
a89b423dfc | ||
![]() |
79e8438941 | ||
![]() |
19c2c46491 | ||
![]() |
78f1d54b69 | ||
![]() |
9461649ed4 | ||
![]() |
8573ab2850 | ||
![]() |
8b3d752727 | ||
![]() |
7c54bd8966 | ||
![]() |
5d0fcb7027 | ||
![]() |
3ec9654d3c | ||
![]() |
7c8d64b462 | ||
![]() |
31e30e3d31 | ||
![]() |
8caf6e99b0 | ||
![]() |
624155debd | ||
![]() |
4468a262ae | ||
![]() |
1780901cb0 | ||
![]() |
d830d9261f |
47
README.md
47
README.md
@@ -4,11 +4,11 @@ MuWire is an easy to use file-sharing program which offers anonymity using [I2P
|
||||
|
||||
It is inspired by the LimeWire Gnutella client and developped by a former LimeWire developer.
|
||||
|
||||
The current stable release - 0.4.0 is avaiable for download at https://muwire.com. You can find technical documentation in the "doc" folder.
|
||||
The current stable release - 0.4.15 is avaiable for download at https://muwire.com. You can find technical documentation in the "doc" folder.
|
||||
|
||||
### Building
|
||||
|
||||
You need JRE 8 or newer. After installing that and setting up the appropriate paths, just type
|
||||
You need JDK 8 or newer. After installing that and setting up the appropriate paths, just type
|
||||
|
||||
```
|
||||
./gradlew clean assemble
|
||||
@@ -19,38 +19,23 @@ If you want to run the unit tests, type
|
||||
./gradlew clean build
|
||||
```
|
||||
|
||||
Some of the UI tests will fail because they haven't been written yet :-/
|
||||
If you want to build binary bundles that do not depend on Java or I2P, see the https://github.com/zlatinb/muwire-pkg project
|
||||
|
||||
### Running
|
||||
|
||||
You need to have an I2P router up and running on the same machine. After you build the application, look inside `gui/build/distributions`. Untar/unzip one of the `shadow` files and then run the jar contained inside by typing `java -jar MuWire-x.y.z.jar` in a terminal or command prompt. If you use a custom I2CP host and port, create a file `$HOME/.MuWire/i2p.properties` and put `i2cp.tcp.host=<host>` and `i2cp.tcp.port=<port>` in there.
|
||||
After you build the application, look inside `gui/build/distributions`. Untar/unzip one of the `shadow` files and then run the jar contained inside by typing `java -jar gui-x.y.z.jar` in a terminal or command prompt.
|
||||
|
||||
The first time you run MuWire it will ask you to select a nickname. This nickname will be displayed with search results, so that others can verify the file was shared by you. It is best to leave MuWire running all the time, just like I2P.
|
||||
If you have an I2P router running on the same machine that is all you need to do. If you use a custom I2CP host and port, create a file `i2p.properties` and put `i2cp.tcp.host=<host>` and `i2cp.tcp.port=<port>` in there. On Windows that file should go into `%HOME%\AppData\Roaming\MuWire`, on Mac into `$HOME/Library/Application Support/MuWire` and on Linux `$HOME/.MuWire`
|
||||
|
||||
[Default I2CP port]\: `7654`
|
||||
|
||||
### GPG Fingerprint
|
||||
|
||||
```
|
||||
471B 9FD4 5517 A5ED 101F C57D A728 3207 2D52 5E41
|
||||
```
|
||||
|
||||
You can find the full key at https://keybase.io/zlatinb
|
||||
|
||||
|
||||
### Known bugs and limitations
|
||||
|
||||
* Many UI features you would expect are not there yet
|
||||
|
||||
### Quick FAQ
|
||||
|
||||
* why is MuWire slow ?
|
||||
|
||||
- too few sources you're downloading from
|
||||
- you can increase the number of tunnels by using more tunnels via Options->I2P Inbound/Outbound Quantity
|
||||
the default is 4 and you could raise up to as high as 16 ( Caution !!!!)
|
||||
|
||||
* my search is not returning (enough) results !
|
||||
|
||||
- search is keyword or hash based
|
||||
- keywords and hash(es) are NOT regexed or wildcarded so they have to be complete
|
||||
so searching for 'musi' will not return results with 'music' - you have to search for 'music'
|
||||
- ALL keywords have to match
|
||||
- only use space for keyword separation
|
||||
- if you already have the file in question it is not displayed ( can be changed via Options )
|
||||
|
||||
* what's this right click -> 'Copy hash to clipboard' for ?
|
||||
|
||||
- if you have a specific file you wish to share or download you can use the hash as a unique identifier
|
||||
to make sure you have exactly the right file.
|
||||
- you can share this hash with others to ensure they are getting the right file
|
||||
[Default I2CP port]: https://geti2p.net/en/docs/ports
|
||||
|
12
TODO.md
12
TODO.md
@@ -12,14 +12,6 @@ This reduces query traffic by not sending last hop queries to peers that definit
|
||||
|
||||
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
|
||||
|
||||
##### Web UI, REST Interface, etc.
|
||||
|
||||
Basically any non-gui non-cli user interface
|
||||
@@ -31,6 +23,4 @@ To enable parsing of metadata from known file types and the user editing it or a
|
||||
### Small Items
|
||||
|
||||
* Wrapper of some kind for in-place upgrades
|
||||
* Download file sequentially
|
||||
* Unsharing of files (half done)
|
||||
* Multiple-selection download, Ctrl-A
|
||||
* Automatic adjustment of number of I2P tunnels
|
||||
|
@@ -2,7 +2,7 @@ subprojects {
|
||||
apply plugin: 'groovy'
|
||||
|
||||
dependencies {
|
||||
compile 'net.i2p:i2p:0.9.40'
|
||||
compile 'net.i2p:i2p:0.9.42'
|
||||
compile 'org.codehaus.groovy:groovy-all:2.4.15'
|
||||
}
|
||||
|
||||
|
@@ -35,7 +35,7 @@ class Cli {
|
||||
|
||||
Core core
|
||||
try {
|
||||
core = new Core(props, home, "0.4.4")
|
||||
core = new Core(props, home, "0.5.0")
|
||||
} 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.4.4")
|
||||
core = new Core(props, home, "0.5.0")
|
||||
} catch (Exception bad) {
|
||||
bad.printStackTrace(System.out)
|
||||
println "Failed to initialize core, exiting"
|
||||
|
@@ -2,9 +2,9 @@ apply plugin : 'application'
|
||||
mainClassName = 'com.muwire.core.Core'
|
||||
applicationDefaultJvmArgs = ['-Djava.util.logging.config.file=logging.properties']
|
||||
dependencies {
|
||||
compile 'net.i2p:router:0.9.40'
|
||||
compile 'net.i2p.client:mstreaming:0.9.40'
|
||||
compile 'net.i2p.client:streaming:0.9.40'
|
||||
compile 'net.i2p:router:0.9.42'
|
||||
compile 'net.i2p.client:mstreaming:0.9.42'
|
||||
compile 'net.i2p.client:streaming:0.9.42'
|
||||
|
||||
testCompile 'org.junit.jupiter:junit-jupiter-api:5.4.2'
|
||||
testCompile 'junit:junit:4.12'
|
||||
|
@@ -1,13 +0,0 @@
|
||||
package com.muwire.core
|
||||
|
||||
import net.i2p.crypto.SigType
|
||||
|
||||
class Constants {
|
||||
public static final byte PERSONA_VERSION = (byte)1
|
||||
public static final SigType SIG_TYPE = SigType.EdDSA_SHA512_Ed25519
|
||||
|
||||
public static final int MAX_HEADER_SIZE = 0x1 << 14
|
||||
public static final int MAX_HEADERS = 16
|
||||
|
||||
public static final String SPLIT_PATTERN = "[\\*\\+\\-,\\.:;\\(\\)=_/\\\\\\!\\\"\\\'\\\$%\\|\\[\\]\\{\\}\\?]"
|
||||
}
|
@@ -20,6 +20,7 @@ import com.muwire.core.download.UIDownloadPausedEvent
|
||||
import com.muwire.core.download.UIDownloadResumedEvent
|
||||
import com.muwire.core.files.FileDownloadedEvent
|
||||
import com.muwire.core.files.FileHashedEvent
|
||||
import com.muwire.core.files.FileHashingEvent
|
||||
import com.muwire.core.files.FileHasher
|
||||
import com.muwire.core.files.FileLoadedEvent
|
||||
import com.muwire.core.files.FileManager
|
||||
@@ -27,6 +28,8 @@ 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.UICommentEvent
|
||||
import com.muwire.core.files.UIPersistFilesEvent
|
||||
import com.muwire.core.files.AllFilesLoadedEvent
|
||||
import com.muwire.core.files.DirectoryUnsharedEvent
|
||||
import com.muwire.core.files.DirectoryWatcher
|
||||
@@ -34,17 +37,23 @@ import com.muwire.core.hostcache.CacheClient
|
||||
import com.muwire.core.hostcache.HostCache
|
||||
import com.muwire.core.hostcache.HostDiscoveredEvent
|
||||
import com.muwire.core.mesh.MeshManager
|
||||
import com.muwire.core.search.BrowseManager
|
||||
import com.muwire.core.search.QueryEvent
|
||||
import com.muwire.core.search.ResultsEvent
|
||||
import com.muwire.core.search.ResultsSender
|
||||
import com.muwire.core.search.SearchEvent
|
||||
import com.muwire.core.search.SearchManager
|
||||
import com.muwire.core.search.UIBrowseEvent
|
||||
import com.muwire.core.search.UIResultBatchEvent
|
||||
import com.muwire.core.trust.TrustEvent
|
||||
import com.muwire.core.trust.TrustService
|
||||
import com.muwire.core.trust.TrustSubscriber
|
||||
import com.muwire.core.trust.TrustSubscriptionEvent
|
||||
import com.muwire.core.update.UpdateClient
|
||||
import com.muwire.core.upload.UploadManager
|
||||
import com.muwire.core.util.MuWireLogManager
|
||||
import com.muwire.core.content.ContentControlEvent
|
||||
import com.muwire.core.content.ContentManager
|
||||
|
||||
import groovy.util.logging.Log
|
||||
import net.i2p.I2PAppContext
|
||||
@@ -74,6 +83,7 @@ public class Core {
|
||||
final MuWireSettings muOptions
|
||||
|
||||
private final TrustService trustService
|
||||
private final TrustSubscriber trustSubscriber
|
||||
private final PersisterService persisterService
|
||||
private final HostCache hostCache
|
||||
private final ConnectionManager connectionManager
|
||||
@@ -86,6 +96,7 @@ public class Core {
|
||||
private final DirectoryWatcher directoryWatcher
|
||||
final FileManager fileManager
|
||||
final UploadManager uploadManager
|
||||
final ContentManager contentManager
|
||||
|
||||
private final Router router
|
||||
|
||||
@@ -128,7 +139,9 @@ public class Core {
|
||||
} else {
|
||||
log.info("launching embedded router")
|
||||
Properties routerProps = new Properties()
|
||||
routerProps.setProperty("i2p.dir.base", home.getAbsolutePath())
|
||||
routerProps.setProperty("i2p.dir.config", home.getAbsolutePath())
|
||||
routerProps.setProperty("router.excludePeerCaps", "KLM")
|
||||
routerProps.setProperty("i2np.inboundKBytesPerSecond", String.valueOf(props.inBw))
|
||||
routerProps.setProperty("i2np.outboundKBytesPerSecond", String.valueOf(props.outBw))
|
||||
routerProps.setProperty("i2cp.disableInterface", "true")
|
||||
@@ -136,7 +149,7 @@ public class Core {
|
||||
routerProps.setProperty("i2np.udp.port", i2pOptions["i2np.udp.port"])
|
||||
routerProps.setProperty("i2np.udp.internalPort", i2pOptions["i2np.udp.port"])
|
||||
router = new Router(routerProps)
|
||||
I2PAppContext.getGlobalContext().metaClass = new RouterContextMetaClass()
|
||||
router.getContext().setLogManager(new MuWireLogManager())
|
||||
router.runRouter()
|
||||
while(!router.isRunning())
|
||||
Thread.sleep(100)
|
||||
@@ -206,14 +219,16 @@ public class Core {
|
||||
eventBus.register(FileUnsharedEvent.class, fileManager)
|
||||
eventBus.register(SearchEvent.class, fileManager)
|
||||
eventBus.register(DirectoryUnsharedEvent.class, fileManager)
|
||||
eventBus.register(UICommentEvent.class, fileManager)
|
||||
|
||||
log.info("initializing mesh manager")
|
||||
MeshManager meshManager = new MeshManager(fileManager, home, props)
|
||||
eventBus.register(SourceDiscoveredEvent.class, meshManager)
|
||||
|
||||
log.info "initializing persistence service"
|
||||
persisterService = new PersisterService(new File(home, "files.json"), eventBus, 15000, fileManager)
|
||||
persisterService = new PersisterService(new File(home, "files.json"), eventBus, 60000, fileManager)
|
||||
eventBus.register(UILoadedEvent.class, persisterService)
|
||||
eventBus.register(UIPersistFilesEvent.class, persisterService)
|
||||
|
||||
log.info("initializing host cache")
|
||||
File hostStorage = new File(home, "hosts.json")
|
||||
@@ -242,7 +257,7 @@ public class Core {
|
||||
I2PConnector i2pConnector = new I2PConnector(socketManager)
|
||||
|
||||
log.info "initializing results sender"
|
||||
ResultsSender resultsSender = new ResultsSender(eventBus, i2pConnector, me)
|
||||
ResultsSender resultsSender = new ResultsSender(eventBus, i2pConnector, me, props)
|
||||
|
||||
log.info "initializing search manager"
|
||||
SearchManager searchManager = new SearchManager(eventBus, me, resultsSender)
|
||||
@@ -268,17 +283,34 @@ public class Core {
|
||||
log.info("initializing acceptor")
|
||||
I2PAcceptor i2pAcceptor = new I2PAcceptor(socketManager)
|
||||
connectionAcceptor = new ConnectionAcceptor(eventBus, connectionManager, props,
|
||||
i2pAcceptor, hostCache, trustService, searchManager, uploadManager, connectionEstablisher)
|
||||
i2pAcceptor, hostCache, trustService, searchManager, uploadManager, fileManager, connectionEstablisher)
|
||||
|
||||
log.info("initializing directory watcher")
|
||||
directoryWatcher = new DirectoryWatcher(eventBus, fileManager)
|
||||
directoryWatcher = new DirectoryWatcher(eventBus, fileManager, home, props)
|
||||
eventBus.register(FileSharedEvent.class, directoryWatcher)
|
||||
eventBus.register(AllFilesLoadedEvent.class, directoryWatcher)
|
||||
eventBus.register(DirectoryUnsharedEvent.class, directoryWatcher)
|
||||
|
||||
log.info("initializing hasher service")
|
||||
hasherService = new HasherService(new FileHasher(), eventBus, fileManager)
|
||||
hasherService = new HasherService(new FileHasher(), eventBus, fileManager, props)
|
||||
eventBus.register(FileSharedEvent.class, hasherService)
|
||||
eventBus.register(FileUnsharedEvent.class, hasherService)
|
||||
eventBus.register(DirectoryUnsharedEvent.class, hasherService)
|
||||
|
||||
log.info("initializing trust subscriber")
|
||||
trustSubscriber = new TrustSubscriber(eventBus, i2pConnector, props)
|
||||
eventBus.register(UILoadedEvent.class, trustSubscriber)
|
||||
eventBus.register(TrustSubscriptionEvent.class, trustSubscriber)
|
||||
|
||||
log.info("initializing content manager")
|
||||
contentManager = new ContentManager()
|
||||
eventBus.register(ContentControlEvent.class, contentManager)
|
||||
eventBus.register(QueryEvent.class, contentManager)
|
||||
|
||||
log.info("initializing browse manager")
|
||||
BrowseManager browseManager = new BrowseManager(i2pConnector, eventBus)
|
||||
eventBus.register(UIBrowseEvent.class, browseManager)
|
||||
|
||||
}
|
||||
|
||||
public void startServices() {
|
||||
@@ -299,6 +331,8 @@ public class Core {
|
||||
log.info("already shutting down")
|
||||
return
|
||||
}
|
||||
log.info("shutting down trust subscriber")
|
||||
trustSubscriber.stop()
|
||||
log.info("shutting down download manageer")
|
||||
downloadManager.shutdown()
|
||||
log.info("shutting down connection acceeptor")
|
||||
@@ -307,6 +341,8 @@ public class Core {
|
||||
connectionEstablisher.stop()
|
||||
log.info("shutting down directory watcher")
|
||||
directoryWatcher.stop()
|
||||
log.info("shutting down cache client")
|
||||
cacheClient.stop()
|
||||
log.info("shutting down connection manager")
|
||||
connectionManager.shutdown()
|
||||
if (router != null) {
|
||||
@@ -315,19 +351,6 @@ public class Core {
|
||||
}
|
||||
}
|
||||
|
||||
static class RouterContextMetaClass extends DelegatingMetaClass {
|
||||
private final Object logManager = new MuWireLogManager()
|
||||
RouterContextMetaClass() {
|
||||
super(RouterContext.class)
|
||||
}
|
||||
|
||||
Object invokeMethod(Object object, String name, Object[] args) {
|
||||
if (name == "logManager")
|
||||
return logManager
|
||||
super.invokeMethod(object, name, args)
|
||||
}
|
||||
}
|
||||
|
||||
static main(args) {
|
||||
def home = System.getProperty("user.home") + File.separator + ".MuWire"
|
||||
home = new File(home)
|
||||
@@ -352,7 +375,7 @@ public class Core {
|
||||
}
|
||||
}
|
||||
|
||||
Core core = new Core(props, home, "0.4.4")
|
||||
Core core = new Core(props, home, "0.5.0")
|
||||
core.startServices()
|
||||
|
||||
// ... at the end, sleep or execute script
|
||||
|
@@ -48,4 +48,9 @@ class EventBus {
|
||||
}
|
||||
currentHandlers.add handler
|
||||
}
|
||||
|
||||
synchronized void unregister(Class<? extends Event> eventType, def handler) {
|
||||
log.info("Unregistering $handler for type $eventType")
|
||||
handlers[eventType]?.remove(handler)
|
||||
}
|
||||
}
|
||||
|
@@ -6,11 +6,16 @@ import com.muwire.core.hostcache.CrawlerResponse
|
||||
import com.muwire.core.util.DataUtil
|
||||
|
||||
import net.i2p.data.Base64
|
||||
import net.i2p.util.ConcurrentHashSet
|
||||
|
||||
class MuWireSettings {
|
||||
|
||||
final boolean isLeaf
|
||||
boolean allowUntrusted
|
||||
boolean searchExtraHop
|
||||
boolean allowTrustLists
|
||||
int trustListInterval
|
||||
Set<Persona> trustSubscriptions
|
||||
int downloadRetryInterval
|
||||
int updateCheckInterval
|
||||
boolean autoDownloadUpdate
|
||||
@@ -19,12 +24,17 @@ class MuWireSettings {
|
||||
File downloadLocation
|
||||
CrawlerResponse crawlerResponse
|
||||
boolean shareDownloadedFiles
|
||||
boolean shareHiddenFiles
|
||||
boolean searchComments
|
||||
boolean browseFiles
|
||||
Set<String> watchedDirectories
|
||||
float downloadSequentialRatio
|
||||
int hostClearInterval
|
||||
int hostClearInterval, hostHopelessInterval, hostRejectInterval
|
||||
int meshExpiration
|
||||
boolean embeddedRouter
|
||||
int inBw, outBw
|
||||
Set<String> watchedKeywords
|
||||
Set<String> watchedRegexes
|
||||
|
||||
MuWireSettings() {
|
||||
this(new Properties())
|
||||
@@ -32,28 +42,42 @@ class MuWireSettings {
|
||||
|
||||
MuWireSettings(Properties props) {
|
||||
isLeaf = Boolean.valueOf(props.get("leaf","false"))
|
||||
allowUntrusted = Boolean.valueOf(props.get("allowUntrusted","true"))
|
||||
allowUntrusted = Boolean.valueOf(props.getProperty("allowUntrusted","true"))
|
||||
searchExtraHop = Boolean.valueOf(props.getProperty("searchExtraHop","false"))
|
||||
allowTrustLists = Boolean.valueOf(props.getProperty("allowTrustLists","true"))
|
||||
trustListInterval = Integer.valueOf(props.getProperty("trustListInterval","1"))
|
||||
crawlerResponse = CrawlerResponse.valueOf(props.get("crawlerResponse","REGISTERED"))
|
||||
nickname = props.getProperty("nickname","MuWireUser")
|
||||
downloadLocation = new File((String)props.getProperty("downloadLocation",
|
||||
System.getProperty("user.home")))
|
||||
downloadRetryInterval = Integer.parseInt(props.getProperty("downloadRetryInterval","1"))
|
||||
downloadRetryInterval = Integer.parseInt(props.getProperty("downloadRetryInterval","60"))
|
||||
updateCheckInterval = Integer.parseInt(props.getProperty("updateCheckInterval","24"))
|
||||
autoDownloadUpdate = Boolean.parseBoolean(props.getProperty("autoDownloadUpdate","true"))
|
||||
updateType = props.getProperty("updateType","jar")
|
||||
shareDownloadedFiles = Boolean.parseBoolean(props.getProperty("shareDownloadedFiles","true"))
|
||||
shareHiddenFiles = Boolean.parseBoolean(props.getProperty("shareHiddenFiles","false"))
|
||||
downloadSequentialRatio = Float.valueOf(props.getProperty("downloadSequentialRatio","0.8"))
|
||||
hostClearInterval = Integer.valueOf(props.getProperty("hostClearInterval","60"))
|
||||
hostClearInterval = Integer.valueOf(props.getProperty("hostClearInterval","15"))
|
||||
hostHopelessInterval = Integer.valueOf(props.getProperty("hostHopelessInterval", "1440"))
|
||||
hostRejectInterval = Integer.valueOf(props.getProperty("hostRejectInterval", "1"))
|
||||
meshExpiration = Integer.valueOf(props.getProperty("meshExpiration","60"))
|
||||
embeddedRouter = Boolean.valueOf(props.getProperty("embeddedRouter","false"))
|
||||
inBw = Integer.valueOf(props.getProperty("inBw","256"))
|
||||
outBw = Integer.valueOf(props.getProperty("outBw","128"))
|
||||
searchComments = Boolean.valueOf(props.getProperty("searchComments","true"))
|
||||
browseFiles = Boolean.valueOf(props.getProperty("browseFiles","true"))
|
||||
|
||||
watchedDirectories = new HashSet<>()
|
||||
if (props.containsKey("watchedDirectories")) {
|
||||
String[] encoded = props.getProperty("watchedDirectories").split(",")
|
||||
encoded.each { watchedDirectories << DataUtil.readi18nString(Base64.decode(it)) }
|
||||
watchedDirectories = readEncodedSet(props, "watchedDirectories")
|
||||
watchedKeywords = readEncodedSet(props, "watchedKeywords")
|
||||
watchedRegexes = readEncodedSet(props, "watchedRegexes")
|
||||
|
||||
trustSubscriptions = new HashSet<>()
|
||||
if (props.containsKey("trustSubscriptions")) {
|
||||
props.getProperty("trustSubscriptions").split(",").each {
|
||||
trustSubscriptions.add(new Persona(new ByteArrayInputStream(Base64.decode(it))))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -61,31 +85,61 @@ class MuWireSettings {
|
||||
Properties props = new Properties()
|
||||
props.setProperty("leaf", isLeaf.toString())
|
||||
props.setProperty("allowUntrusted", allowUntrusted.toString())
|
||||
props.setProperty("searchExtraHop", String.valueOf(searchExtraHop))
|
||||
props.setProperty("allowTrustLists", String.valueOf(allowTrustLists))
|
||||
props.setProperty("trustListInterval", String.valueOf(trustListInterval))
|
||||
props.setProperty("crawlerResponse", crawlerResponse.toString())
|
||||
props.setProperty("nickname", nickname)
|
||||
props.setProperty("downloadLocation", downloadLocation.getAbsolutePath())
|
||||
props.setProperty("downloadRetryInterval", String.valueOf(downloadRetryInterval))
|
||||
props.setProperty("updateCheckInterval", String.valueOf(updateCheckInterval))
|
||||
props.setProperty("autoDownloadUpdate", String.valueOf(autoDownloadUpdate))
|
||||
props.setProperty("updateType",updateType)
|
||||
props.setProperty("updateType",String.valueOf(updateType))
|
||||
props.setProperty("shareDownloadedFiles", String.valueOf(shareDownloadedFiles))
|
||||
props.setProperty("shareHiddenFiles", String.valueOf(shareHiddenFiles))
|
||||
props.setProperty("downloadSequentialRatio", String.valueOf(downloadSequentialRatio))
|
||||
props.setProperty("hostClearInterval", String.valueOf(hostClearInterval))
|
||||
props.setProperty("hostHopelessInterval", String.valueOf(hostHopelessInterval))
|
||||
props.setProperty("hostRejectInterval", String.valueOf(hostRejectInterval))
|
||||
props.setProperty("meshExpiration", String.valueOf(meshExpiration))
|
||||
props.setProperty("embeddedRouter", String.valueOf(embeddedRouter))
|
||||
props.setProperty("inBw", String.valueOf(inBw))
|
||||
props.setProperty("outBw", String.valueOf(outBw))
|
||||
props.setProperty("searchComments", String.valueOf(searchComments))
|
||||
props.setProperty("browseFiles", String.valueOf(browseFiles))
|
||||
|
||||
if (!watchedDirectories.isEmpty()) {
|
||||
String encoded = watchedDirectories.stream().
|
||||
map({Base64.encode(DataUtil.encodei18nString(it))}).
|
||||
writeEncodedSet(watchedDirectories, "watchedDirectories", props)
|
||||
writeEncodedSet(watchedKeywords, "watchedKeywords", props)
|
||||
writeEncodedSet(watchedRegexes, "watchedRegexes", props)
|
||||
|
||||
if (!trustSubscriptions.isEmpty()) {
|
||||
String encoded = trustSubscriptions.stream().
|
||||
map({it.toBase64()}).
|
||||
collect(Collectors.joining(","))
|
||||
props.setProperty("watchedDirectories", encoded)
|
||||
props.setProperty("trustSubscriptions", encoded)
|
||||
}
|
||||
|
||||
props.store(out, "")
|
||||
}
|
||||
|
||||
private static Set<String> readEncodedSet(Properties props, String property) {
|
||||
Set<String> rv = new ConcurrentHashSet<>()
|
||||
if (props.containsKey(property)) {
|
||||
String[] encoded = props.getProperty(property).split(",")
|
||||
encoded.each { rv << DataUtil.readi18nString(Base64.decode(it)) }
|
||||
}
|
||||
rv
|
||||
}
|
||||
|
||||
private static void writeEncodedSet(Set<String> set, String property, Properties props) {
|
||||
if (set.isEmpty())
|
||||
return
|
||||
String encoded = set.stream().
|
||||
map({Base64.encode(DataUtil.encodei18nString(it))}).
|
||||
collect(Collectors.joining(","))
|
||||
props.setProperty(property, encoded)
|
||||
}
|
||||
|
||||
boolean isLeaf() {
|
||||
isLeaf
|
||||
}
|
||||
|
7
core/src/main/groovy/com/muwire/core/SplitPattern.groovy
Normal file
7
core/src/main/groovy/com/muwire/core/SplitPattern.groovy
Normal file
@@ -0,0 +1,7 @@
|
||||
package com.muwire.core
|
||||
|
||||
class SplitPattern {
|
||||
|
||||
public static final String SPLIT_PATTERN = "[\\*\\+\\-,\\.:;\\(\\)=_/\\\\\\!\\\"\\\'\\\$%\\|\\[\\]\\{\\}\\?]";
|
||||
|
||||
}
|
@@ -22,6 +22,9 @@ import net.i2p.data.Destination
|
||||
@Log
|
||||
abstract class Connection implements Closeable {
|
||||
|
||||
private static final int SEARCHES = 10
|
||||
private static final long INTERVAL = 1000
|
||||
|
||||
final EventBus eventBus
|
||||
final Endpoint endpoint
|
||||
final boolean incoming
|
||||
@@ -32,6 +35,7 @@ abstract class Connection implements Closeable {
|
||||
private final AtomicBoolean running = new AtomicBoolean()
|
||||
private final BlockingQueue messages = new LinkedBlockingQueue()
|
||||
private final Thread reader, writer
|
||||
private final LinkedList<Long> searchTimestamps = new LinkedList<>()
|
||||
|
||||
protected final String name
|
||||
|
||||
@@ -128,6 +132,7 @@ abstract class Connection implements Closeable {
|
||||
query.firstHop = e.firstHop
|
||||
query.keywords = e.searchEvent.getSearchTerms()
|
||||
query.oobInfohash = e.searchEvent.oobInfohash
|
||||
query.searchComments = e.searchEvent.searchComments
|
||||
if (e.searchEvent.searchHash != null)
|
||||
query.infohash = Base64.encode(e.searchEvent.searchHash)
|
||||
query.replyTo = e.replyTo.toBase64()
|
||||
@@ -156,7 +161,25 @@ abstract class Connection implements Closeable {
|
||||
}
|
||||
}
|
||||
|
||||
private boolean throttleSearch() {
|
||||
final long now = System.currentTimeMillis()
|
||||
if (searchTimestamps.size() < SEARCHES) {
|
||||
searchTimestamps.addLast(now)
|
||||
return false
|
||||
}
|
||||
Long oldest = searchTimestamps.getFirst()
|
||||
if (now - oldest.longValue() < INTERVAL)
|
||||
return true
|
||||
searchTimestamps.addLast(now)
|
||||
searchTimestamps.removeFirst()
|
||||
false
|
||||
}
|
||||
|
||||
protected void handleSearch(def search) {
|
||||
if (throttleSearch()) {
|
||||
log.info("dropping excessive search")
|
||||
return
|
||||
}
|
||||
UUID uuid = UUID.fromString(search.uuid)
|
||||
byte [] infohash = null
|
||||
if (search.infohash != null) {
|
||||
@@ -187,11 +210,15 @@ abstract class Connection implements Closeable {
|
||||
boolean oob = false
|
||||
if (search.oobInfohash != null)
|
||||
oob = search.oobInfohash
|
||||
boolean searchComments = false
|
||||
if (search.searchComments != null)
|
||||
searchComments = search.searchComments
|
||||
|
||||
SearchEvent searchEvent = new SearchEvent(searchTerms : search.keywords,
|
||||
searchHash : infohash,
|
||||
uuid : uuid,
|
||||
oobInfohash : oob)
|
||||
oobInfohash : oob,
|
||||
searchComments : searchComments)
|
||||
QueryEvent event = new QueryEvent ( searchEvent : searchEvent,
|
||||
replyTo : replyTo,
|
||||
originator : originator,
|
||||
|
@@ -10,12 +10,15 @@ import java.util.zip.InflaterInputStream
|
||||
import com.muwire.core.EventBus
|
||||
import com.muwire.core.MuWireSettings
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.files.FileManager
|
||||
import com.muwire.core.hostcache.HostCache
|
||||
import com.muwire.core.trust.TrustLevel
|
||||
import com.muwire.core.trust.TrustService
|
||||
import com.muwire.core.upload.UploadManager
|
||||
import com.muwire.core.util.DataUtil
|
||||
import com.muwire.core.search.InvalidSearchResultException
|
||||
import com.muwire.core.search.ResultsParser
|
||||
import com.muwire.core.search.ResultsSender
|
||||
import com.muwire.core.search.SearchManager
|
||||
import com.muwire.core.search.UIResultBatchEvent
|
||||
import com.muwire.core.search.UIResultEvent
|
||||
@@ -36,6 +39,7 @@ class ConnectionAcceptor {
|
||||
final TrustService trustService
|
||||
final SearchManager searchManager
|
||||
final UploadManager uploadManager
|
||||
final FileManager fileManager
|
||||
final ConnectionEstablisher establisher
|
||||
|
||||
final ExecutorService acceptorThread
|
||||
@@ -46,7 +50,7 @@ class ConnectionAcceptor {
|
||||
ConnectionAcceptor(EventBus eventBus, UltrapeerConnectionManager manager,
|
||||
MuWireSettings settings, I2PAcceptor acceptor, HostCache hostCache,
|
||||
TrustService trustService, SearchManager searchManager, UploadManager uploadManager,
|
||||
ConnectionEstablisher establisher) {
|
||||
FileManager fileManager, ConnectionEstablisher establisher) {
|
||||
this.eventBus = eventBus
|
||||
this.manager = manager
|
||||
this.settings = settings
|
||||
@@ -54,6 +58,7 @@ class ConnectionAcceptor {
|
||||
this.hostCache = hostCache
|
||||
this.trustService = trustService
|
||||
this.searchManager = searchManager
|
||||
this.fileManager = fileManager
|
||||
this.uploadManager = uploadManager
|
||||
this.establisher = establisher
|
||||
|
||||
@@ -125,6 +130,12 @@ class ConnectionAcceptor {
|
||||
case (byte)'P':
|
||||
processPOST(e)
|
||||
break
|
||||
case (byte)'T':
|
||||
processTRUST(e)
|
||||
break
|
||||
case (byte)'B':
|
||||
processBROWSE(e)
|
||||
break
|
||||
default:
|
||||
throw new Exception("Invalid read $read")
|
||||
}
|
||||
@@ -243,4 +254,85 @@ class ConnectionAcceptor {
|
||||
}
|
||||
}
|
||||
|
||||
private void processBROWSE(Endpoint e) {
|
||||
try {
|
||||
byte [] rowse = new byte[7]
|
||||
DataInputStream dis = new DataInputStream(e.getInputStream())
|
||||
dis.readFully(rowse)
|
||||
if (rowse != "ROWSE\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
throw new IOException("Invalid BROWSE connection")
|
||||
String header
|
||||
while ((header = DataUtil.readTillRN(dis)) != ""); // ignore headers for now
|
||||
|
||||
OutputStream os = e.getOutputStream()
|
||||
if (!settings.browseFiles) {
|
||||
os.write("403 Not Allowed\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
os.flush()
|
||||
e.close()
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
os.write("200 OK\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
|
||||
def sharedFiles = fileManager.getSharedFiles().values()
|
||||
|
||||
os.write("Count: ${sharedFiles.size()}\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
|
||||
DataOutputStream dos = new DataOutputStream(os)
|
||||
JsonOutput jsonOutput = new JsonOutput()
|
||||
sharedFiles.each {
|
||||
def obj = ResultsSender.sharedFileToObj(it, false)
|
||||
def json = jsonOutput.toJson(obj)
|
||||
dos.writeShort((short)json.length())
|
||||
dos.write(json.getBytes(StandardCharsets.US_ASCII))
|
||||
}
|
||||
dos.flush()
|
||||
} finally {
|
||||
e.close()
|
||||
}
|
||||
}
|
||||
|
||||
private void processTRUST(Endpoint e) {
|
||||
try {
|
||||
byte[] RUST = new byte[6]
|
||||
DataInputStream dis = new DataInputStream(e.getInputStream())
|
||||
dis.readFully(RUST)
|
||||
if (RUST != "RUST\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
throw new IOException("Invalid TRUST connection")
|
||||
String header
|
||||
while ((header = DataUtil.readTillRN(dis)) != ""); // ignore headers for now
|
||||
|
||||
OutputStream os = e.getOutputStream()
|
||||
if (!settings.allowTrustLists) {
|
||||
os.write("403 Not Allowed\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
os.flush()
|
||||
e.close()
|
||||
return
|
||||
}
|
||||
|
||||
os.write("200 OK\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
List<Persona> good = new ArrayList<>(trustService.good.values())
|
||||
int size = Math.min(Short.MAX_VALUE * 2, good.size())
|
||||
good = good.subList(0, size)
|
||||
DataOutputStream dos = new DataOutputStream(os)
|
||||
dos.writeShort(size)
|
||||
good.each {
|
||||
it.write(dos)
|
||||
}
|
||||
|
||||
List<Persona> bad = new ArrayList<>(trustService.bad.values())
|
||||
size = Math.min(Short.MAX_VALUE * 2, bad.size())
|
||||
bad = bad.subList(0, size)
|
||||
dos.writeShort(size)
|
||||
bad.each {
|
||||
it.write(dos)
|
||||
}
|
||||
|
||||
dos.flush()
|
||||
} finally {
|
||||
e.close()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -31,7 +31,7 @@ class ConnectionEstablisher {
|
||||
final HostCache hostCache
|
||||
|
||||
final Timer timer
|
||||
final ExecutorService executor
|
||||
final ExecutorService executor, closer
|
||||
|
||||
final Set inProgress = new ConcurrentHashSet()
|
||||
|
||||
@@ -51,6 +51,8 @@ class ConnectionEstablisher {
|
||||
rv.setName("connector-${System.currentTimeMillis()}")
|
||||
rv
|
||||
} as ThreadFactory)
|
||||
|
||||
closer = Executors.newSingleThreadExecutor()
|
||||
}
|
||||
|
||||
void start() {
|
||||
@@ -60,6 +62,7 @@ class ConnectionEstablisher {
|
||||
void stop() {
|
||||
timer.cancel()
|
||||
executor.shutdownNow()
|
||||
closer.shutdown()
|
||||
}
|
||||
|
||||
private void connectIfNeeded() {
|
||||
@@ -120,8 +123,10 @@ class ConnectionEstablisher {
|
||||
}
|
||||
|
||||
private void fail(Endpoint endpoint) {
|
||||
closer.execute {
|
||||
endpoint.close()
|
||||
eventBus.publish(new ConnectionEvent(endpoint: endpoint, incoming: false, leaf: false, status: ConnectionAttemptStatus.FAILED))
|
||||
} as Runnable
|
||||
}
|
||||
|
||||
private void readK(Endpoint e) {
|
||||
@@ -175,7 +180,7 @@ class ConnectionEstablisher {
|
||||
log.log(Level.WARNING,"Problem parsing post-rejection payload",ignore)
|
||||
} finally {
|
||||
// the end
|
||||
e.close()
|
||||
closer.execute({e.close()} as Runnable)
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -0,0 +1,9 @@
|
||||
package com.muwire.core.content
|
||||
|
||||
import com.muwire.core.Event
|
||||
|
||||
class ContentControlEvent extends Event {
|
||||
String term
|
||||
boolean regex
|
||||
boolean add
|
||||
}
|
@@ -0,0 +1,30 @@
|
||||
package com.muwire.core.content
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
import com.muwire.core.search.QueryEvent
|
||||
|
||||
import net.i2p.util.ConcurrentHashSet
|
||||
|
||||
class ContentManager {
|
||||
|
||||
Set<Matcher> matchers = new ConcurrentHashSet()
|
||||
|
||||
void onContentControlEvent(ContentControlEvent e) {
|
||||
Matcher m
|
||||
if (e.regex)
|
||||
m = new RegexMatcher(e.term)
|
||||
else
|
||||
m = new KeywordMatcher(e.term)
|
||||
if (e.add)
|
||||
matchers.add(m)
|
||||
else
|
||||
matchers.remove(m)
|
||||
}
|
||||
|
||||
void onQueryEvent(QueryEvent e) {
|
||||
if (e.searchEvent.searchTerms == null)
|
||||
return
|
||||
matchers.each { it.process(e) }
|
||||
}
|
||||
}
|
@@ -0,0 +1,36 @@
|
||||
package com.muwire.core.content
|
||||
|
||||
class KeywordMatcher extends Matcher {
|
||||
private final String keyword
|
||||
KeywordMatcher(String keyword) {
|
||||
this.keyword = keyword
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean match(List<String> searchTerms) {
|
||||
boolean found = false
|
||||
searchTerms.each {
|
||||
if (keyword == it)
|
||||
found = true
|
||||
}
|
||||
found
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTerm() {
|
||||
keyword
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
keyword.hashCode()
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (!(o instanceof KeywordMatcher))
|
||||
return false
|
||||
KeywordMatcher other = (KeywordMatcher) o
|
||||
keyword.equals(other.keyword)
|
||||
}
|
||||
}
|
@@ -0,0 +1,9 @@
|
||||
package com.muwire.core.content
|
||||
|
||||
import com.muwire.core.Persona
|
||||
|
||||
class Match {
|
||||
Persona persona
|
||||
String [] keywords
|
||||
long timestamp
|
||||
}
|
20
core/src/main/groovy/com/muwire/core/content/Matcher.groovy
Normal file
20
core/src/main/groovy/com/muwire/core/content/Matcher.groovy
Normal file
@@ -0,0 +1,20 @@
|
||||
package com.muwire.core.content
|
||||
|
||||
import com.muwire.core.search.QueryEvent
|
||||
|
||||
abstract class Matcher {
|
||||
final List<Match> matches = Collections.synchronizedList(new ArrayList<>())
|
||||
final Set<UUID> uuids = new HashSet<>()
|
||||
|
||||
protected abstract boolean match(List<String> searchTerms);
|
||||
|
||||
public abstract String getTerm();
|
||||
|
||||
public void process(QueryEvent qe) {
|
||||
def terms = qe.searchEvent.searchTerms
|
||||
if (match(terms) && uuids.add(qe.searchEvent.uuid)) {
|
||||
long now = System.currentTimeMillis()
|
||||
matches << new Match(persona : qe.originator, keywords : terms, timestamp : now)
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,35 @@
|
||||
package com.muwire.core.content
|
||||
|
||||
import java.util.regex.Pattern
|
||||
import java.util.stream.Collectors
|
||||
|
||||
class RegexMatcher extends Matcher {
|
||||
private final Pattern pattern
|
||||
RegexMatcher(String pattern) {
|
||||
this.pattern = Pattern.compile(pattern)
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean match(List<String> keywords) {
|
||||
String combined = keywords.join(" ")
|
||||
return pattern.matcher(combined).find()
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTerm() {
|
||||
pattern.pattern()
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
pattern.pattern().hashCode()
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (!(o instanceof RegexMatcher))
|
||||
return false
|
||||
RegexMatcher other = (RegexMatcher) o
|
||||
pattern.pattern() == other.pattern.pattern()
|
||||
}
|
||||
}
|
@@ -74,7 +74,7 @@ public class DownloadManager {
|
||||
destinations.addAll(e.sources)
|
||||
destinations.remove(me.destination)
|
||||
|
||||
Pieces pieces = getPieces(infohash, size, pieceSize)
|
||||
Pieces pieces = getPieces(infohash, size, pieceSize, e.sequential)
|
||||
|
||||
def downloader = new Downloader(eventBus, this, me, e.target, size,
|
||||
infohash, pieceSize, connector, destinations,
|
||||
@@ -123,7 +123,11 @@ public class DownloadManager {
|
||||
infoHash = new InfoHash(root)
|
||||
}
|
||||
|
||||
Pieces pieces = getPieces(infoHash, (long)json.length, json.pieceSizePow2)
|
||||
boolean sequential = false
|
||||
if (json.sequential != null)
|
||||
sequential = json.sequential
|
||||
|
||||
Pieces pieces = getPieces(infoHash, (long)json.length, json.pieceSizePow2, sequential)
|
||||
|
||||
def downloader = new Downloader(eventBus, this, me, file, (long)json.length,
|
||||
infoHash, json.pieceSizePow2, connector, destinations, incompletes, pieces)
|
||||
@@ -137,12 +141,12 @@ public class DownloadManager {
|
||||
}
|
||||
}
|
||||
|
||||
private Pieces getPieces(InfoHash infoHash, long length, int pieceSizePow2) {
|
||||
private Pieces getPieces(InfoHash infoHash, long length, int pieceSizePow2, boolean sequential) {
|
||||
int pieceSize = 0x1 << pieceSizePow2
|
||||
int nPieces = (int)(length / pieceSize)
|
||||
if (length % pieceSize != 0)
|
||||
nPieces++
|
||||
Mesh mesh = meshManager.getOrCreate(infoHash, nPieces)
|
||||
Mesh mesh = meshManager.getOrCreate(infoHash, nPieces, sequential)
|
||||
mesh.pieces
|
||||
}
|
||||
|
||||
@@ -188,6 +192,9 @@ public class DownloadManager {
|
||||
json.hashRoot = Base64.encode(infoHash.getRoot())
|
||||
|
||||
json.paused = downloader.paused
|
||||
|
||||
json.sequential = downloader.pieces.ratio == 0f
|
||||
|
||||
writer.println(JsonOutput.toJson(json))
|
||||
}
|
||||
}
|
||||
|
@@ -14,6 +14,7 @@ import static com.muwire.core.util.DataUtil.readTillRN
|
||||
import groovy.util.logging.Log
|
||||
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.MappedByteBuffer
|
||||
import java.nio.channels.FileChannel
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.nio.file.Files
|
||||
@@ -25,8 +26,6 @@ import java.util.logging.Level
|
||||
@Log
|
||||
class DownloadSession {
|
||||
|
||||
private static int SAMPLES = 10
|
||||
|
||||
private final EventBus eventBus
|
||||
private final String meB64
|
||||
private final Pieces pieces
|
||||
@@ -38,10 +37,10 @@ class DownloadSession {
|
||||
private final Set<Integer> available
|
||||
private final MessageDigest digest
|
||||
|
||||
private final LinkedList<Long> timestamps = new LinkedList<>()
|
||||
private final LinkedList<Integer> reads = new LinkedList<>()
|
||||
private long lastSpeedRead = System.currentTimeMillis()
|
||||
private long dataSinceLastRead
|
||||
|
||||
private ByteBuffer mapped
|
||||
private MappedByteBuffer mapped
|
||||
|
||||
DownloadSession(EventBus eventBus, String meB64, Pieces pieces, InfoHash infoHash, Endpoint endpoint, File file,
|
||||
int pieceSize, long fileLength, Set<Integer> available) {
|
||||
@@ -71,21 +70,23 @@ class DownloadSession {
|
||||
OutputStream os = endpoint.getOutputStream()
|
||||
InputStream is = endpoint.getInputStream()
|
||||
|
||||
int piece
|
||||
int[] pieceAndPosition
|
||||
if (available.isEmpty())
|
||||
piece = pieces.claim()
|
||||
pieceAndPosition = pieces.claim()
|
||||
else
|
||||
piece = pieces.claim(new HashSet<>(available))
|
||||
if (piece == -1)
|
||||
pieceAndPosition = pieces.claim(new HashSet<>(available))
|
||||
if (pieceAndPosition == null)
|
||||
return false
|
||||
int piece = pieceAndPosition[0]
|
||||
int position = pieceAndPosition[1]
|
||||
boolean steal = pieceAndPosition[2] == 1
|
||||
boolean unclaim = true
|
||||
|
||||
log.info("will download piece $piece")
|
||||
|
||||
long start = piece * pieceSize
|
||||
long end = Math.min(fileLength, start + pieceSize) - 1
|
||||
long length = end - start + 1
|
||||
log.info("will download piece $piece from position $position steal $steal")
|
||||
|
||||
long pieceStart = piece * ((long)pieceSize)
|
||||
long end = Math.min(fileLength, pieceStart + pieceSize) - 1
|
||||
long start = pieceStart + position
|
||||
String root = Base64.encode(infoHash.getRoot())
|
||||
|
||||
try {
|
||||
@@ -174,8 +175,9 @@ class DownloadSession {
|
||||
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)
|
||||
StandardOpenOption.SPARSE, StandardOpenOption.CREATE))
|
||||
mapped = channel.map(FileChannel.MapMode.READ_WRITE, pieceStart, end - pieceStart + 1)
|
||||
mapped.position(position)
|
||||
|
||||
byte[] tmp = new byte[0x1 << 13]
|
||||
while(mapped.hasRemaining()) {
|
||||
@@ -186,31 +188,28 @@ class DownloadSession {
|
||||
throw new IOException()
|
||||
synchronized(this) {
|
||||
mapped.put(tmp, 0, read)
|
||||
|
||||
if (timestamps.size() == SAMPLES) {
|
||||
timestamps.removeFirst()
|
||||
reads.removeFirst()
|
||||
}
|
||||
timestamps.addLast(System.currentTimeMillis())
|
||||
reads.addLast(read)
|
||||
dataSinceLastRead += read
|
||||
pieces.markPartial(piece, mapped.position())
|
||||
}
|
||||
}
|
||||
|
||||
mapped.clear()
|
||||
digest.update(mapped)
|
||||
DataUtil.tryUnmap(mapped)
|
||||
byte [] hash = digest.digest()
|
||||
byte [] expected = new byte[32]
|
||||
System.arraycopy(infoHash.getHashList(), piece * 32, expected, 0, 32)
|
||||
if (hash != expected)
|
||||
throw new BadHashException()
|
||||
if (hash != expected) {
|
||||
pieces.markPartial(piece, 0)
|
||||
throw new BadHashException("bad hash on piece $piece")
|
||||
}
|
||||
} finally {
|
||||
try { channel?.close() } catch (IOException ignore) {}
|
||||
DataUtil.tryUnmap(mapped)
|
||||
}
|
||||
pieces.markDownloaded(piece)
|
||||
unclaim = false
|
||||
} finally {
|
||||
if (unclaim)
|
||||
if (unclaim && !steal)
|
||||
pieces.unclaim(piece)
|
||||
}
|
||||
return true
|
||||
@@ -223,24 +222,11 @@ class DownloadSession {
|
||||
}
|
||||
|
||||
synchronized int speed() {
|
||||
if (timestamps.size() < SAMPLES)
|
||||
return 0
|
||||
int totalRead = 0
|
||||
int idx = 0
|
||||
final long now = System.currentTimeMillis()
|
||||
|
||||
while(idx < SAMPLES && timestamps.get(idx) < now - 1000)
|
||||
idx++
|
||||
if (idx == SAMPLES)
|
||||
return 0
|
||||
if (idx == SAMPLES - 1)
|
||||
return reads[idx]
|
||||
|
||||
long interval = timestamps.last - timestamps[idx]
|
||||
if (interval == 0)
|
||||
interval = 1
|
||||
for (int i = idx; i < SAMPLES; i++)
|
||||
totalRead += reads[idx]
|
||||
(int)(totalRead * 1000.0 / interval)
|
||||
long interval = Math.max(1000, now - lastSpeedRead)
|
||||
lastSpeedRead = now;
|
||||
int rv = (int) (dataSinceLastRead * 1000.0 / interval)
|
||||
dataSinceLastRead = 0
|
||||
rv
|
||||
}
|
||||
}
|
||||
|
@@ -7,6 +7,7 @@ import com.muwire.core.connection.Endpoint
|
||||
import java.nio.file.AtomicMoveNotSupportedException
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.StandardCopyOption
|
||||
import java.time.Instant
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Executors
|
||||
@@ -58,6 +59,11 @@ public class Downloader {
|
||||
private final AtomicBoolean eventFired = new AtomicBoolean()
|
||||
private boolean piecesFileClosed
|
||||
|
||||
private ArrayList speedArr = new ArrayList<Integer>()
|
||||
private int speedPos = 0
|
||||
private int speedAvg = 0
|
||||
private long timestamp = Instant.now().toEpochMilli()
|
||||
|
||||
public Downloader(EventBus eventBus, DownloadManager downloadManager,
|
||||
Persona me, File file, long length, InfoHash infoHash,
|
||||
int pieceSizePow2, I2PConnector connector, Set<Destination> destinations,
|
||||
@@ -76,6 +82,10 @@ public class Downloader {
|
||||
this.pieceSize = 1 << pieceSizePow2
|
||||
this.pieces = pieces
|
||||
this.nPieces = pieces.nPieces
|
||||
|
||||
// default size suitable for an average of 5 seconds / 5 elements / 5 interval units
|
||||
// it's easily adjustable by resizing the size of speedArr
|
||||
this.speedArr = [ 0, 0, 0, 0, 0 ]
|
||||
}
|
||||
|
||||
public synchronized InfoHash getInfoHash() {
|
||||
@@ -101,8 +111,14 @@ public class Downloader {
|
||||
if (!piecesFile.exists())
|
||||
return
|
||||
piecesFile.eachLine {
|
||||
int piece = Integer.parseInt(it)
|
||||
String [] split = it.split(",")
|
||||
int piece = Integer.parseInt(split[0])
|
||||
if (split.length == 1)
|
||||
pieces.markDownloaded(piece)
|
||||
else {
|
||||
int position = Integer.parseInt(split[1])
|
||||
pieces.markPartial(piece, position)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,9 +127,7 @@ public class Downloader {
|
||||
if (piecesFileClosed)
|
||||
return
|
||||
piecesFile.withPrintWriter { writer ->
|
||||
pieces.getDownloaded().each { piece ->
|
||||
writer.println(piece)
|
||||
}
|
||||
pieces.write(writer)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -124,14 +138,35 @@ public class Downloader {
|
||||
|
||||
|
||||
public int speed() {
|
||||
int total = 0
|
||||
int currSpeed = 0
|
||||
if (getCurrentState() == DownloadState.DOWNLOADING) {
|
||||
activeWorkers.values().each {
|
||||
if (it.currentState == WorkerState.DOWNLOADING)
|
||||
total += it.speed()
|
||||
currSpeed += it.speed()
|
||||
}
|
||||
}
|
||||
total
|
||||
|
||||
// normalize to speedArr.size
|
||||
currSpeed /= speedArr.size()
|
||||
|
||||
// compute new speedAvg and update speedArr
|
||||
if ( speedArr[speedPos] > speedAvg ) {
|
||||
speedAvg = 0
|
||||
} else {
|
||||
speedAvg -= speedArr[speedPos]
|
||||
}
|
||||
speedAvg += currSpeed
|
||||
speedArr[speedPos] = currSpeed
|
||||
// this might be necessary due to rounding errors
|
||||
if (speedAvg < 0)
|
||||
speedAvg = 0
|
||||
|
||||
// rolling index over the speedArr
|
||||
speedPos++
|
||||
if (speedPos >= speedArr.size())
|
||||
speedPos=0
|
||||
|
||||
speedAvg
|
||||
}
|
||||
|
||||
public DownloadState getCurrentState() {
|
||||
@@ -272,12 +307,17 @@ public class Downloader {
|
||||
} catch (Exception bad) {
|
||||
log.log(Level.WARNING,"Exception while downloading",DataUtil.findRoot(bad))
|
||||
} finally {
|
||||
writePieces()
|
||||
currentState = WorkerState.FINISHED
|
||||
if (pieces.isComplete() && eventFired.compareAndSet(false, true)) {
|
||||
synchronized(piecesFile) {
|
||||
piecesFileClosed = true
|
||||
piecesFile.delete()
|
||||
}
|
||||
activeWorkers.values().each {
|
||||
if (it.destination != destination)
|
||||
it.cancel()
|
||||
}
|
||||
try {
|
||||
Files.move(incompleteFile.toPath(), file.toPath(), StandardCopyOption.ATOMIC_MOVE)
|
||||
} catch (AtomicMoveNotSupportedException e) {
|
||||
@@ -286,7 +326,7 @@ public class Downloader {
|
||||
}
|
||||
eventBus.publish(
|
||||
new FileDownloadedEvent(
|
||||
downloadedFile : new DownloadedFile(file, getInfoHash(), pieceSizePow2, successfulDestinations),
|
||||
downloadedFile : new DownloadedFile(file.getCanonicalFile(), getInfoHash(), pieceSizePow2, successfulDestinations),
|
||||
downloader : Downloader.this))
|
||||
|
||||
}
|
||||
|
@@ -5,6 +5,7 @@ class Pieces {
|
||||
private final int nPieces
|
||||
private final float ratio
|
||||
private final Random random = new Random()
|
||||
private final Map<Integer,Integer> partials = new HashMap<>()
|
||||
|
||||
Pieces(int nPieces) {
|
||||
this(nPieces, 1.0f)
|
||||
@@ -17,16 +18,22 @@ class Pieces {
|
||||
claimed = new BitSet(nPieces)
|
||||
}
|
||||
|
||||
synchronized int claim() {
|
||||
synchronized int[] claim() {
|
||||
int claimedCardinality = claimed.cardinality()
|
||||
if (claimedCardinality == nPieces)
|
||||
return -1
|
||||
if (claimedCardinality == nPieces) {
|
||||
// steal
|
||||
int downloadedCardinality = done.cardinality()
|
||||
if (downloadedCardinality == nPieces)
|
||||
return null
|
||||
int rv = done.nextClearBit(0)
|
||||
return [rv, partials.getOrDefault(rv, 0), 1]
|
||||
}
|
||||
|
||||
// if fuller than ratio just do sequential
|
||||
if ( (1.0f * claimedCardinality) / nPieces > ratio) {
|
||||
if ( (1.0f * claimedCardinality) / nPieces >= ratio) {
|
||||
int rv = claimed.nextClearBit(0)
|
||||
claimed.set(rv)
|
||||
return rv
|
||||
return [rv, partials.getOrDefault(rv, 0), 0]
|
||||
}
|
||||
|
||||
while(true) {
|
||||
@@ -34,20 +41,29 @@ class Pieces {
|
||||
if (claimed.get(start))
|
||||
continue
|
||||
claimed.set(start)
|
||||
return start
|
||||
return [start, partials.getOrDefault(start,0), 0]
|
||||
}
|
||||
}
|
||||
|
||||
synchronized int claim(Set<Integer> available) {
|
||||
for (int i = claimed.nextSetBit(0); i >= 0; i = claimed.nextSetBit(i+1))
|
||||
synchronized int[] claim(Set<Integer> available) {
|
||||
for (int i = done.nextSetBit(0); i >= 0; i = done.nextSetBit(i+1))
|
||||
available.remove(i)
|
||||
if (available.isEmpty())
|
||||
return -1
|
||||
List<Integer> toList = available.toList()
|
||||
return null
|
||||
Set<Integer> availableCopy = new HashSet<>(available)
|
||||
for (int i = claimed.nextSetBit(0); i >= 0; i = claimed.nextSetBit(i+1))
|
||||
availableCopy.remove(i)
|
||||
if (availableCopy.isEmpty()) {
|
||||
// steal
|
||||
int rv = available.first()
|
||||
return [rv, partials.getOrDefault(rv, 0), 1]
|
||||
}
|
||||
List<Integer> toList = availableCopy.toList()
|
||||
if (ratio > 0f)
|
||||
Collections.shuffle(toList)
|
||||
int rv = toList[0]
|
||||
claimed.set(rv)
|
||||
rv
|
||||
[rv, partials.getOrDefault(rv, 0), 0]
|
||||
}
|
||||
|
||||
synchronized def getDownloaded() {
|
||||
@@ -61,6 +77,11 @@ class Pieces {
|
||||
synchronized void markDownloaded(int piece) {
|
||||
done.set(piece)
|
||||
claimed.set(piece)
|
||||
partials.remove(piece)
|
||||
}
|
||||
|
||||
synchronized void markPartial(int piece, int position) {
|
||||
partials.put(piece, position)
|
||||
}
|
||||
|
||||
synchronized void unclaim(int piece) {
|
||||
@@ -82,5 +103,15 @@ class Pieces {
|
||||
synchronized void clearAll() {
|
||||
done.clear()
|
||||
claimed.clear()
|
||||
partials.clear()
|
||||
}
|
||||
|
||||
synchronized void write(PrintWriter writer) {
|
||||
for (int i = done.nextSetBit(0); i >= 0; i = done.nextSetBit(i+1)) {
|
||||
writer.println(i)
|
||||
}
|
||||
partials.each { piece, position ->
|
||||
writer.println("$piece,$position")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -10,4 +10,5 @@ class UIDownloadEvent extends Event {
|
||||
UIResultEvent[] result
|
||||
Set<Destination> sources
|
||||
File target
|
||||
boolean sequential
|
||||
}
|
||||
|
@@ -13,6 +13,7 @@ import java.nio.file.WatchService
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
import com.muwire.core.EventBus
|
||||
import com.muwire.core.MuWireSettings
|
||||
import com.muwire.core.SharedFile
|
||||
|
||||
import groovy.util.logging.Log
|
||||
@@ -31,6 +32,8 @@ class DirectoryWatcher {
|
||||
kinds = [ENTRY_CREATE, ENTRY_MODIFY, ENTRY_DELETE]
|
||||
}
|
||||
|
||||
private final File home
|
||||
private final MuWireSettings muOptions
|
||||
private final EventBus eventBus
|
||||
private final FileManager fileManager
|
||||
private final Thread watcherThread, publisherThread
|
||||
@@ -39,7 +42,9 @@ class DirectoryWatcher {
|
||||
private WatchService watchService
|
||||
private volatile boolean shutdown
|
||||
|
||||
DirectoryWatcher(EventBus eventBus, FileManager fileManager) {
|
||||
DirectoryWatcher(EventBus eventBus, FileManager fileManager, File home, MuWireSettings muOptions) {
|
||||
this.home = home
|
||||
this.muOptions = muOptions
|
||||
this.eventBus = eventBus
|
||||
this.fileManager = fileManager
|
||||
this.watcherThread = new Thread({watch() } as Runnable, "directory-watcher")
|
||||
@@ -64,15 +69,28 @@ class DirectoryWatcher {
|
||||
void onFileSharedEvent(FileSharedEvent e) {
|
||||
if (!e.file.isDirectory())
|
||||
return
|
||||
Path path = e.file.getCanonicalFile().toPath()
|
||||
File canonical = e.file.getCanonicalFile()
|
||||
Path path = canonical.toPath()
|
||||
WatchKey wk = path.register(watchService, kinds)
|
||||
watchedDirectories.put(e.file, wk)
|
||||
watchedDirectories.put(canonical, wk)
|
||||
|
||||
if (muOptions.watchedDirectories.add(canonical.toString()))
|
||||
saveMuSettings()
|
||||
}
|
||||
|
||||
void onDirectoryUnsharedEvent(DirectoryUnsharedEvent e) {
|
||||
WatchKey wk = watchedDirectories.remove(e.directory)
|
||||
wk?.cancel()
|
||||
|
||||
if (muOptions.watchedDirectories.remove(e.directory.toString()))
|
||||
saveMuSettings()
|
||||
}
|
||||
|
||||
private void saveMuSettings() {
|
||||
File muSettingsFile = new File(home, "MuWire.properties")
|
||||
muSettingsFile.withOutputStream {
|
||||
muOptions.write(it)
|
||||
}
|
||||
}
|
||||
|
||||
private void watch() {
|
||||
@@ -120,7 +138,7 @@ class DirectoryWatcher {
|
||||
|
||||
private static File join(Path parent, Path path) {
|
||||
File parentFile = parent.toFile().getCanonicalFile()
|
||||
new File(parentFile, path.toFile().getName())
|
||||
new File(parentFile, path.toFile().getName()).getCanonicalFile()
|
||||
}
|
||||
|
||||
private void publish() {
|
||||
|
@@ -0,0 +1,15 @@
|
||||
package com.muwire.core.files
|
||||
|
||||
import com.muwire.core.Event
|
||||
import com.muwire.core.SharedFile
|
||||
|
||||
class FileHashingEvent extends Event {
|
||||
|
||||
File hashingFile
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
super.toString() + " hashingFile " + hashingFile.getAbsolutePath()
|
||||
}
|
||||
|
||||
}
|
@@ -8,8 +8,10 @@ import com.muwire.core.UILoadedEvent
|
||||
import com.muwire.core.search.ResultsEvent
|
||||
import com.muwire.core.search.SearchEvent
|
||||
import com.muwire.core.search.SearchIndex
|
||||
import com.muwire.core.util.DataUtil
|
||||
|
||||
import groovy.util.logging.Log
|
||||
import net.i2p.data.Base64
|
||||
|
||||
@Log
|
||||
class FileManager {
|
||||
@@ -20,6 +22,7 @@ class FileManager {
|
||||
final Map<InfoHash, Set<SharedFile>> rootToFiles = Collections.synchronizedMap(new HashMap<>())
|
||||
final Map<File, SharedFile> fileToSharedFile = Collections.synchronizedMap(new HashMap<>())
|
||||
final Map<String, Set<File>> nameToFiles = new HashMap<>()
|
||||
final Map<String, Set<File>> commentToFile = new HashMap<>()
|
||||
final SearchIndex index = new SearchIndex()
|
||||
|
||||
FileManager(EventBus eventBus, MuWireSettings settings) {
|
||||
@@ -62,6 +65,18 @@ class FileManager {
|
||||
}
|
||||
existingFiles.add(sf.getFile())
|
||||
|
||||
String comment = sf.getComment()
|
||||
if (comment != null) {
|
||||
comment = DataUtil.readi18nString(Base64.decode(comment))
|
||||
index.add(comment)
|
||||
Set<File> existingComment = commentToFile.get(comment)
|
||||
if(existingComment == null) {
|
||||
existingComment = new HashSet<>()
|
||||
commentToFile.put(comment, existingComment)
|
||||
}
|
||||
existingComment.add(sf.getFile())
|
||||
}
|
||||
|
||||
index.add(name)
|
||||
}
|
||||
|
||||
@@ -87,9 +102,45 @@ class FileManager {
|
||||
}
|
||||
}
|
||||
|
||||
String comment = sf.getComment()
|
||||
if (comment != null) {
|
||||
Set<File> existingComment = commentToFile.get(comment)
|
||||
if (existingComment != null) {
|
||||
existingComment.remove(sf.getFile())
|
||||
if (existingComment.isEmpty()) {
|
||||
commentToFile.remove(comment)
|
||||
index.remove(comment)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
index.remove(name)
|
||||
}
|
||||
|
||||
void onUICommentEvent(UICommentEvent e) {
|
||||
if (e.oldComment != null) {
|
||||
def comment = DataUtil.readi18nString(Base64.decode(e.oldComment))
|
||||
Set<File> existingFiles = commentToFile.get(comment)
|
||||
existingFiles.remove(e.sharedFile.getFile())
|
||||
if (existingFiles.isEmpty()) {
|
||||
commentToFile.remove(comment)
|
||||
index.remove(comment)
|
||||
}
|
||||
}
|
||||
|
||||
String comment = e.sharedFile.getComment()
|
||||
comment = DataUtil.readi18nString(Base64.decode(comment))
|
||||
if (comment != null) {
|
||||
index.add(comment)
|
||||
Set<File> existingComment = commentToFile.get(comment)
|
||||
if(existingComment == null) {
|
||||
existingComment = new HashSet<>()
|
||||
commentToFile.put(comment, existingComment)
|
||||
}
|
||||
existingComment.add(e.sharedFile.getFile())
|
||||
}
|
||||
}
|
||||
|
||||
Map<File, SharedFile> getSharedFiles() {
|
||||
synchronized(fileToSharedFile) {
|
||||
return new HashMap<>(fileToSharedFile)
|
||||
@@ -112,10 +163,15 @@ class FileManager {
|
||||
} else {
|
||||
def names = index.search e.searchTerms
|
||||
Set<File> files = new HashSet<>()
|
||||
names.each { files.addAll nameToFiles.getOrDefault(it, []) }
|
||||
names.each {
|
||||
files.addAll nameToFiles.getOrDefault(it, [])
|
||||
if (e.searchComments)
|
||||
files.addAll commentToFile.getOrDefault(it, [])
|
||||
}
|
||||
Set<SharedFile> sharedFiles = new HashSet<>()
|
||||
files.each { sharedFiles.add fileToSharedFile[it] }
|
||||
files = filter(sharedFiles, e.oobInfohash)
|
||||
|
||||
if (!sharedFiles.isEmpty())
|
||||
re = new ResultsEvent(results: sharedFiles.asList(), uuid: e.uuid, searchEvent: e)
|
||||
|
||||
|
@@ -4,6 +4,7 @@ import java.util.concurrent.Executor
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
import com.muwire.core.EventBus
|
||||
import com.muwire.core.MuWireSettings
|
||||
import com.muwire.core.SharedFile
|
||||
|
||||
class HasherService {
|
||||
@@ -11,12 +12,15 @@ class HasherService {
|
||||
final FileHasher hasher
|
||||
final EventBus eventBus
|
||||
final FileManager fileManager
|
||||
final Set<File> hashed = new HashSet<>()
|
||||
final MuWireSettings settings
|
||||
Executor executor
|
||||
|
||||
HasherService(FileHasher hasher, EventBus eventBus, FileManager fileManager) {
|
||||
HasherService(FileHasher hasher, EventBus eventBus, FileManager fileManager, MuWireSettings settings) {
|
||||
this.hasher = hasher
|
||||
this.eventBus = eventBus
|
||||
this.fileManager = fileManager
|
||||
this.settings = settings
|
||||
}
|
||||
|
||||
void start() {
|
||||
@@ -24,13 +28,24 @@ class HasherService {
|
||||
}
|
||||
|
||||
void onFileSharedEvent(FileSharedEvent evt) {
|
||||
if (fileManager.fileToSharedFile.containsKey(evt.file))
|
||||
File canonical = evt.file.getCanonicalFile()
|
||||
if (!settings.shareHiddenFiles && canonical.isHidden())
|
||||
return
|
||||
executor.execute( { -> process(evt.file) } as Runnable)
|
||||
if (fileManager.fileToSharedFile.containsKey(canonical))
|
||||
return
|
||||
if (hashed.add(canonical))
|
||||
executor.execute( { -> process(canonical) } as Runnable)
|
||||
}
|
||||
|
||||
void onFileUnsharedEvent(FileUnsharedEvent evt) {
|
||||
hashed.remove(evt.unsharedFile.file)
|
||||
}
|
||||
|
||||
void onDirectoryUnsharedEvent(DirectoryUnsharedEvent evt) {
|
||||
hashed.remove(evt.directory)
|
||||
}
|
||||
|
||||
private void process(File f) {
|
||||
f = f.getCanonicalFile()
|
||||
if (f.isDirectory()) {
|
||||
f.listFiles().each {eventBus.publish new FileSharedEvent(file: it) }
|
||||
} else {
|
||||
@@ -39,6 +54,7 @@ class HasherService {
|
||||
} else if (f.length() > FileHasher.MAX_SIZE) {
|
||||
eventBus.publish new FileHashedEvent(error: "$f is too large to be shared ${f.length()}")
|
||||
} else {
|
||||
eventBus.publish new FileHashingEvent(hashingFile: f)
|
||||
def hash = hasher.hashFile f
|
||||
eventBus.publish new FileHashedEvent(sharedFile: new SharedFile(f, hash, FileHasher.getPieceSize(f.length())))
|
||||
}
|
||||
|
@@ -3,6 +3,9 @@ package com.muwire.core.files
|
||||
import java.nio.file.CopyOption
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.StandardCopyOption
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.ThreadFactory
|
||||
import java.util.logging.Level
|
||||
import java.util.stream.Collectors
|
||||
|
||||
@@ -28,13 +31,16 @@ class PersisterService extends Service {
|
||||
final int interval
|
||||
final Timer timer
|
||||
final FileManager fileManager
|
||||
final ExecutorService persisterExecutor = Executors.newSingleThreadExecutor({ r ->
|
||||
new Thread(r, "file persister")
|
||||
} as ThreadFactory)
|
||||
|
||||
PersisterService(File location, EventBus listener, int interval, FileManager fileManager) {
|
||||
this.location = location
|
||||
this.listener = listener
|
||||
this.interval = interval
|
||||
this.fileManager = fileManager
|
||||
timer = new Timer("file persister", true)
|
||||
timer = new Timer("file persister timer", true)
|
||||
}
|
||||
|
||||
void stop() {
|
||||
@@ -45,8 +51,15 @@ class PersisterService extends Service {
|
||||
timer.schedule({load()} as TimerTask, 1)
|
||||
}
|
||||
|
||||
void onUIPersistFilesEvent(UIPersistFilesEvent e) {
|
||||
persistFiles()
|
||||
}
|
||||
|
||||
void load() {
|
||||
Thread.currentThread().setPriority(Thread.MIN_PRIORITY)
|
||||
|
||||
if (location.exists() && location.isFile()) {
|
||||
int loaded = 0
|
||||
def slurper = new JsonSlurper()
|
||||
try {
|
||||
location.eachLine {
|
||||
@@ -56,6 +69,9 @@ class PersisterService extends Service {
|
||||
if (event != null) {
|
||||
log.fine("loaded file $event.loadedFile.file")
|
||||
listener.publish event
|
||||
loaded++
|
||||
if (loaded % 10 == 0)
|
||||
Thread.sleep(20)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -109,16 +125,19 @@ class PersisterService extends Service {
|
||||
List sources = (List)json.sources
|
||||
Set<Destination> sourceSet = sources.stream().map({d -> new Destination(d.toString())}).collect Collectors.toSet()
|
||||
DownloadedFile df = new DownloadedFile(file, ih, pieceSize, sourceSet)
|
||||
df.setComment(json.comment)
|
||||
return new FileLoadedEvent(loadedFile : df)
|
||||
}
|
||||
|
||||
|
||||
SharedFile sf = new SharedFile(file, ih, pieceSize)
|
||||
sf.setComment(json.comment)
|
||||
return new FileLoadedEvent(loadedFile: sf)
|
||||
|
||||
}
|
||||
|
||||
private void persistFiles() {
|
||||
persisterExecutor.submit( {
|
||||
def sharedFiles = fileManager.getSharedFiles()
|
||||
|
||||
File tmp = File.createTempFile("muwire-files", "tmp")
|
||||
@@ -132,21 +151,18 @@ class PersisterService extends Service {
|
||||
}
|
||||
Files.copy(tmp.toPath(), location.toPath(), StandardCopyOption.REPLACE_EXISTING)
|
||||
tmp.delete()
|
||||
} as Runnable)
|
||||
}
|
||||
|
||||
private def toJson(File f, SharedFile sf) {
|
||||
def json = [:]
|
||||
json.file = Base64.encode DataUtil.encodei18nString(f.getCanonicalFile().toString())
|
||||
json.length = f.length()
|
||||
json.file = sf.getB64EncodedFileName()
|
||||
json.length = sf.getCachedLength()
|
||||
InfoHash ih = sf.getInfoHash()
|
||||
json.infoHash = Base64.encode ih.getRoot()
|
||||
json.infoHash = sf.getB64EncodedHashRoot()
|
||||
json.pieceSize = sf.getPieceSize()
|
||||
byte [] tmp = new byte [32]
|
||||
json.hashList = []
|
||||
for (int i = 0;i < ih.getHashList().length / 32; i++) {
|
||||
System.arraycopy(ih.getHashList(), i * 32, tmp, 0, 32)
|
||||
json.hashList.add Base64.encode(tmp)
|
||||
}
|
||||
json.hashList = sf.getB64EncodedHashList()
|
||||
json.comment = sf.getComment()
|
||||
|
||||
if (sf instanceof DownloadedFile) {
|
||||
json.sources = sf.sources.stream().map( {d -> d.toBase64()}).collect(Collectors.toList())
|
||||
|
@@ -0,0 +1,9 @@
|
||||
package com.muwire.core.files
|
||||
|
||||
import com.muwire.core.Event
|
||||
import com.muwire.core.SharedFile
|
||||
|
||||
class UICommentEvent extends Event {
|
||||
SharedFile sharedFile
|
||||
String oldComment
|
||||
}
|
@@ -0,0 +1,6 @@
|
||||
package com.muwire.core.files
|
||||
|
||||
import com.muwire.core.Event
|
||||
|
||||
class UIPersistFilesEvent extends Event {
|
||||
}
|
@@ -6,7 +6,12 @@ class CacheServers {
|
||||
|
||||
private static final int TO_GIVE = 3
|
||||
private static Set<Destination> CACHES = [
|
||||
new Destination("Wddh2E6FyyXBF7SvUYHKdN-vjf3~N6uqQWNeBDTM0P33YjiQCOsyedrjmDZmWFrXUJfJLWnCb5bnKezfk4uDaMyj~uvDG~yvLVcFgcPWSUd7BfGgym-zqcG1q1DcM8vfun-US7YamBlmtC6MZ2j-~Igqzmgshita8aLPCfNAA6S6e2UMjjtG7QIXlxpMec75dkHdJlVWbzrk9z8Qgru3YIk0UztYgEwDNBbm9wInsbHhr3HtAfa02QcgRVqRN2PnQXuqUJs7R7~09FZPEviiIcUpkY3FeyLlX1sgQFBeGeA96blaPvZNGd6KnNdgfLgMebx5SSxC-N4KZMSMBz5cgonQF3~m2HHFRSI85zqZNG5X9bJN85t80ltiv1W1es8ZnQW4es11r7MrvJNXz5bmSH641yJIvS6qI8OJJNpFVBIQSXLD-96TayrLQPaYw~uNZ-eXaE6G5dYhiuN8xHsFI1QkdaUaVZnvDGfsRbpS5GtpUbBDbyLkdPurG0i7dN1wAAAA")
|
||||
// zlatinb
|
||||
new Destination("Wddh2E6FyyXBF7SvUYHKdN-vjf3~N6uqQWNeBDTM0P33YjiQCOsyedrjmDZmWFrXUJfJLWnCb5bnKezfk4uDaMyj~uvDG~yvLVcFgcPWSUd7BfGgym-zqcG1q1DcM8vfun-US7YamBlmtC6MZ2j-~Igqzmgshita8aLPCfNAA6S6e2UMjjtG7QIXlxpMec75dkHdJlVWbzrk9z8Qgru3YIk0UztYgEwDNBbm9wInsbHhr3HtAfa02QcgRVqRN2PnQXuqUJs7R7~09FZPEviiIcUpkY3FeyLlX1sgQFBeGeA96blaPvZNGd6KnNdgfLgMebx5SSxC-N4KZMSMBz5cgonQF3~m2HHFRSI85zqZNG5X9bJN85t80ltiv1W1es8ZnQW4es11r7MrvJNXz5bmSH641yJIvS6qI8OJJNpFVBIQSXLD-96TayrLQPaYw~uNZ-eXaE6G5dYhiuN8xHsFI1QkdaUaVZnvDGfsRbpS5GtpUbBDbyLkdPurG0i7dN1wAAAA"),
|
||||
// sNL
|
||||
new Destination("JC63wJNOqSJmymkj4~UJWywBTvDGikKMoYP0HX2Wz9c5l3otXSkwnxWAFL4cKr~Ygh3BNNi2t93vuLIiI1W8AsE42kR~PwRx~Y-WvIHXR6KUejRmOp-n8WidtjKg9k4aDy428uSOedqXDxys5mpoeQXwDsv1CoPTTwnmb1GWFy~oTGIsCguCl~aJWGnqiKarPO3GJQ~ev-NbvAQzUfC3HeP1e6pdI5CGGjExahTCID5UjpJw8GaDXWlGmYWWH303Xu4x-vAHQy1dJLsOBCn8dZravsn5BKJk~j0POUon45CCx-~NYtaPe0Itt9cMdD2ciC76Rep1D0X0sm1SjlSs8sZ52KmF3oaLZ6OzgI9QLMIyBUrfi41sK5I0qTuUVBAkvW1xr~L-20dYJ9TrbOaOb2-vDIfKaxVi6xQOuhgQDiSBhd3qv2m0xGu-BM9DQYfNA0FdMjnZmqjmji9RMavzQSsVFIbQGLbrLepiEFlb7TseCK5UtRp8TxnG7L4gbYevBQAEAAcAAA=="),
|
||||
// dark_trion
|
||||
new Destination("Gec9L29FVcQvYDgpcYuEYdltJn06PPoOWAcAM8Af-gDm~ehlrJcwlLXXs0hidq~yP2A0X7QcDi6i6shAfuEofTchxGJl8LRNqj9lio7WnB7cIixXWL~uCkD7Np5LMX0~akNX34oOb9RcBYVT2U5rFGJmJ7OtBv~IBkGeLhsMrqaCjahd0jdBO~QJ-t82ZKZhh044d24~JEfF9zSJxdBoCdAcXzryGNy7sYtFVDFsPKJudAxSW-UsSQiGw2~k-TxyF0r-iAt1IdzfNu8Lu0WPqLdhDYJWcPldx2PR5uJorI~zo~z3I5RX3NwzarlbD4nEP5s65ahPSfVCEkzmaJUBgP8DvBqlFaX89K4nGRYc7jkEjJ8cX4L6YPXUpTPWcfKkW259WdQY3YFh6x7rzijrGZewpczOLCrt-bZRYgDrUibmZxKZmNhy~lQu4gYVVjkz1i4tL~DWlhIc4y0x2vItwkYLArPPi~ejTnt-~Lhb7oPMXRcWa3UrwGKpFvGZY4NXBQAEAAcAAA==")
|
||||
]
|
||||
|
||||
static List<Destination> getCacheServers() {
|
||||
|
@@ -7,21 +7,35 @@ class Host {
|
||||
private static final int MAX_FAILURES = 3
|
||||
|
||||
final Destination destination
|
||||
private final int clearInterval
|
||||
private final int clearInterval, hopelessInterval, rejectionInterval
|
||||
int failures,successes
|
||||
long lastAttempt
|
||||
long lastSuccessfulAttempt
|
||||
long lastRejection
|
||||
|
||||
public Host(Destination destination, int clearInterval) {
|
||||
public Host(Destination destination, int clearInterval, int hopelessInterval, int rejectionInterval) {
|
||||
this.destination = destination
|
||||
this.clearInterval = clearInterval
|
||||
this.hopelessInterval = hopelessInterval
|
||||
this.rejectionInterval = rejectionInterval
|
||||
}
|
||||
|
||||
synchronized void onConnect() {
|
||||
private void connectSuccessful() {
|
||||
failures = 0
|
||||
successes++
|
||||
lastAttempt = System.currentTimeMillis()
|
||||
}
|
||||
|
||||
synchronized void onConnect() {
|
||||
connectSuccessful()
|
||||
lastSuccessfulAttempt = lastAttempt
|
||||
}
|
||||
|
||||
synchronized void onReject() {
|
||||
connectSuccessful()
|
||||
lastRejection = lastAttempt;
|
||||
}
|
||||
|
||||
synchronized void onFailure() {
|
||||
failures++
|
||||
successes = 0
|
||||
@@ -40,7 +54,17 @@ class Host {
|
||||
failures = 0
|
||||
}
|
||||
|
||||
synchronized void canTryAgain() {
|
||||
synchronized boolean canTryAgain() {
|
||||
lastSuccessfulAttempt > 0 &&
|
||||
System.currentTimeMillis() - lastAttempt > (clearInterval * 60 * 1000)
|
||||
}
|
||||
|
||||
synchronized boolean isHopeless() {
|
||||
isFailed() &&
|
||||
System.currentTimeMillis() - lastSuccessfulAttempt > (hopelessInterval * 60 * 1000)
|
||||
}
|
||||
|
||||
synchronized boolean isRecentlyRejected() {
|
||||
System.currentTimeMillis() - lastRejection < (rejectionInterval * 60 * 1000)
|
||||
}
|
||||
}
|
||||
|
@@ -52,7 +52,7 @@ class HostCache extends Service {
|
||||
hosts.get(e.destination).clearFailures()
|
||||
return
|
||||
}
|
||||
Host host = new Host(e.destination, settings.hostClearInterval)
|
||||
Host host = new Host(e.destination, settings.hostClearInterval, settings.hostHopelessInterval, settings.hostRejectInterval)
|
||||
if (allowHost(host)) {
|
||||
hosts.put(e.destination, host)
|
||||
}
|
||||
@@ -64,15 +64,17 @@ class HostCache extends Service {
|
||||
Destination dest = e.endpoint.destination
|
||||
Host host = hosts.get(dest)
|
||||
if (host == null) {
|
||||
host = new Host(dest, settings.hostClearInterval)
|
||||
host = new Host(dest, settings.hostClearInterval, settings.hostHopelessInterval, settings.hostRejectInterval)
|
||||
hosts.put(dest, host)
|
||||
}
|
||||
|
||||
switch(e.status) {
|
||||
case ConnectionAttemptStatus.SUCCESSFUL:
|
||||
case ConnectionAttemptStatus.REJECTED:
|
||||
host.onConnect()
|
||||
break
|
||||
case ConnectionAttemptStatus.REJECTED:
|
||||
host.onReject()
|
||||
break
|
||||
case ConnectionAttemptStatus.FAILED:
|
||||
host.onFailure()
|
||||
break
|
||||
@@ -82,6 +84,10 @@ class HostCache extends Service {
|
||||
List<Destination> getHosts(int n) {
|
||||
List<Destination> rv = new ArrayList<>(hosts.keySet())
|
||||
rv.retainAll {allowHost(hosts[it])}
|
||||
rv.removeAll {
|
||||
def h = hosts[it];
|
||||
(h.isFailed() && !h.canTryAgain()) || h.isRecentlyRejected()
|
||||
}
|
||||
if (rv.size() <= n)
|
||||
return rv
|
||||
Collections.shuffle(rv)
|
||||
@@ -106,11 +112,15 @@ class HostCache extends Service {
|
||||
storage.eachLine {
|
||||
def entry = slurper.parseText(it)
|
||||
Destination dest = new Destination(entry.destination)
|
||||
Host host = new Host(dest, settings.hostClearInterval)
|
||||
Host host = new Host(dest, settings.hostClearInterval, settings.hostHopelessInterval, settings.hostRejectInterval)
|
||||
host.failures = Integer.valueOf(String.valueOf(entry.failures))
|
||||
host.successes = Integer.valueOf(String.valueOf(entry.successes))
|
||||
if (entry.lastAttempt != null)
|
||||
host.lastAttempt = entry.lastAttempt
|
||||
if (entry.lastSuccessfulAttempt != null)
|
||||
host.lastSuccessfulAttempt = entry.lastSuccessfulAttempt
|
||||
if (entry.lastRejection != null)
|
||||
host.lastRejection = entry.lastRejection
|
||||
if (allowHost(host))
|
||||
hosts.put(dest, host)
|
||||
}
|
||||
@@ -120,8 +130,6 @@ class HostCache extends Service {
|
||||
}
|
||||
|
||||
private boolean allowHost(Host host) {
|
||||
if (host.isFailed() && !host.canTryAgain())
|
||||
return false
|
||||
if (host.destination == myself)
|
||||
return false
|
||||
TrustLevel trust = trustService.getLevel(host.destination)
|
||||
@@ -140,12 +148,14 @@ class HostCache extends Service {
|
||||
storage.delete()
|
||||
storage.withPrintWriter { writer ->
|
||||
hosts.each { dest, host ->
|
||||
if (allowHost(host)) {
|
||||
if (allowHost(host) && !host.isHopeless()) {
|
||||
def map = [:]
|
||||
map.destination = dest.toBase64()
|
||||
map.failures = host.failures
|
||||
map.successes = host.successes
|
||||
map.lastAttempt = host.lastAttempt
|
||||
map.lastSuccessfulAttempt = host.lastSuccessfulAttempt
|
||||
map.lastRejection = host.lastRejection
|
||||
def json = JsonOutput.toJson(map)
|
||||
writer.println json
|
||||
}
|
||||
|
@@ -33,11 +33,12 @@ class MeshManager {
|
||||
meshes.get(infoHash)
|
||||
}
|
||||
|
||||
Mesh getOrCreate(InfoHash infoHash, int nPieces) {
|
||||
Mesh getOrCreate(InfoHash infoHash, int nPieces, boolean sequential) {
|
||||
synchronized(meshes) {
|
||||
if (meshes.containsKey(infoHash))
|
||||
return meshes.get(infoHash)
|
||||
Pieces pieces = new Pieces(nPieces, settings.downloadSequentialRatio)
|
||||
float ratio = sequential ? 0f : settings.downloadSequentialRatio
|
||||
Pieces pieces = new Pieces(nPieces, ratio)
|
||||
if (fileManager.rootToFiles.containsKey(infoHash)) {
|
||||
for (int i = 0; i < nPieces; i++)
|
||||
pieces.markDownloaded(i)
|
||||
|
@@ -0,0 +1,86 @@
|
||||
package com.muwire.core.search
|
||||
|
||||
import com.muwire.core.Constants
|
||||
import com.muwire.core.EventBus
|
||||
import com.muwire.core.connection.Endpoint
|
||||
import com.muwire.core.connection.I2PConnector
|
||||
import com.muwire.core.util.DataUtil
|
||||
|
||||
import groovy.json.JsonSlurper
|
||||
import groovy.util.logging.Log
|
||||
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util.concurrent.Executor
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.logging.Level
|
||||
|
||||
@Log
|
||||
class BrowseManager {
|
||||
|
||||
private final I2PConnector connector
|
||||
private final EventBus eventBus
|
||||
|
||||
private final Executor browserThread = Executors.newSingleThreadExecutor()
|
||||
|
||||
BrowseManager(I2PConnector connector, EventBus eventBus) {
|
||||
this.connector = connector
|
||||
this.eventBus = eventBus
|
||||
}
|
||||
|
||||
void onUIBrowseEvent(UIBrowseEvent e) {
|
||||
browserThread.execute({
|
||||
Endpoint endpoint = null
|
||||
try {
|
||||
eventBus.publish(new BrowseStatusEvent(status : BrowseStatus.CONNECTING))
|
||||
endpoint = connector.connect(e.host.destination)
|
||||
OutputStream os = endpoint.getOutputStream()
|
||||
os.write("BROWSE\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
|
||||
InputStream is = endpoint.getInputStream()
|
||||
String code = DataUtil.readTillRN(is)
|
||||
if (!code.startsWith("200"))
|
||||
throw new IOException("Invalid code")
|
||||
|
||||
// parse all headers
|
||||
Map<String,String> headers = new HashMap<>()
|
||||
String header
|
||||
while((header = DataUtil.readTillRN(is)) != "" && headers.size() < Constants.MAX_HEADERS) {
|
||||
int colon = header.indexOf(':')
|
||||
if (colon == -1 || colon == header.length() - 1)
|
||||
throw new IOException("invalid header $header")
|
||||
String key = header.substring(0, colon)
|
||||
String value = header.substring(colon + 1)
|
||||
headers[key] = value.trim()
|
||||
}
|
||||
|
||||
if (!headers.containsKey("Count"))
|
||||
throw new IOException("No count header")
|
||||
|
||||
int results = Integer.parseInt(headers['Count'])
|
||||
|
||||
// at this stage, start pulling the results
|
||||
eventBus.publish(new BrowseStatusEvent(status : BrowseStatus.FETCHING))
|
||||
|
||||
JsonSlurper slurper = new JsonSlurper()
|
||||
DataInputStream dis = new DataInputStream(is)
|
||||
UUID uuid = UUID.randomUUID()
|
||||
for (int i = 0; i < results; i++) {
|
||||
int size = dis.readUnsignedShort()
|
||||
byte [] tmp = new byte[size]
|
||||
dis.readFully(tmp)
|
||||
def json = slurper.parse(tmp)
|
||||
UIResultEvent result = ResultsParser.parse(e.host, uuid, json)
|
||||
eventBus.publish(result)
|
||||
}
|
||||
|
||||
eventBus.publish(new BrowseStatusEvent(status : BrowseStatus.FINISHED))
|
||||
|
||||
} catch (Exception bad) {
|
||||
log.log(Level.WARNING, "browse failed", bad)
|
||||
eventBus.publish(new BrowseStatusEvent(status : BrowseStatus.FAILED))
|
||||
} finally {
|
||||
endpoint?.close()
|
||||
}
|
||||
} as Runnable)
|
||||
}
|
||||
}
|
@@ -0,0 +1,5 @@
|
||||
package com.muwire.core.search;
|
||||
|
||||
public enum BrowseStatus {
|
||||
CONNECTING, FETCHING, FINISHED, FAILED
|
||||
}
|
@@ -0,0 +1,7 @@
|
||||
package com.muwire.core.search
|
||||
|
||||
import com.muwire.core.Event
|
||||
|
||||
class BrowseStatusEvent extends Event {
|
||||
BrowseStatus status
|
||||
}
|
@@ -91,12 +91,22 @@ class ResultsParser {
|
||||
if (json.sources != null)
|
||||
sources = json.sources.stream().map({new Destination(it)}).collect(Collectors.toSet())
|
||||
|
||||
String comment = null
|
||||
if (json.comment != null)
|
||||
comment = DataUtil.readi18nString(Base64.decode(json.comment))
|
||||
|
||||
boolean browse = false
|
||||
if (json.browse != null)
|
||||
browse = true
|
||||
|
||||
return new UIResultEvent( sender : p,
|
||||
name : name,
|
||||
size : size,
|
||||
infohash : new InfoHash(infoHash),
|
||||
pieceSize : pieceSize,
|
||||
sources : sources,
|
||||
comment : comment,
|
||||
browse : browse,
|
||||
uuid: uuid)
|
||||
} catch (Exception e) {
|
||||
throw new InvalidSearchResultException("parsing search result failed",e)
|
||||
|
@@ -4,6 +4,7 @@ import com.muwire.core.SharedFile
|
||||
import com.muwire.core.connection.Endpoint
|
||||
import com.muwire.core.connection.I2PConnector
|
||||
import com.muwire.core.files.FileHasher
|
||||
import com.muwire.core.util.DataUtil
|
||||
import com.muwire.core.Persona
|
||||
|
||||
import java.nio.charset.StandardCharsets
|
||||
@@ -17,6 +18,7 @@ import java.util.stream.Collectors
|
||||
import com.muwire.core.DownloadedFile
|
||||
import com.muwire.core.EventBus
|
||||
import com.muwire.core.InfoHash
|
||||
import com.muwire.core.MuWireSettings
|
||||
|
||||
import groovy.json.JsonOutput
|
||||
import groovy.util.logging.Log
|
||||
@@ -42,11 +44,13 @@ class ResultsSender {
|
||||
private final I2PConnector connector
|
||||
private final Persona me
|
||||
private final EventBus eventBus
|
||||
private final MuWireSettings settings
|
||||
|
||||
ResultsSender(EventBus eventBus, I2PConnector connector, Persona me) {
|
||||
ResultsSender(EventBus eventBus, I2PConnector connector, Persona me, MuWireSettings settings) {
|
||||
this.connector = connector;
|
||||
this.eventBus = eventBus
|
||||
this.me = me
|
||||
this.settings = settings
|
||||
}
|
||||
|
||||
void sendResults(UUID uuid, SharedFile[] results, Destination target, boolean oobInfohash) {
|
||||
@@ -60,13 +64,18 @@ class ResultsSender {
|
||||
Set<Destination> suggested = Collections.emptySet()
|
||||
if (it instanceof DownloadedFile)
|
||||
suggested = it.sources
|
||||
def comment = null
|
||||
if (it.getComment() != null) {
|
||||
comment = DataUtil.readi18nString(Base64.decode(it.getComment()))
|
||||
}
|
||||
def uiResultEvent = new UIResultEvent( sender : me,
|
||||
name : it.getFile().getName(),
|
||||
size : length,
|
||||
infohash : it.getInfoHash(),
|
||||
pieceSize : pieceSize,
|
||||
uuid : uuid,
|
||||
sources : suggested
|
||||
sources : suggested,
|
||||
comment : comment
|
||||
)
|
||||
eventBus.publish(uiResultEvent)
|
||||
}
|
||||
@@ -85,7 +94,6 @@ class ResultsSender {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
byte [] tmp = new byte[InfoHash.SIZE]
|
||||
JsonOutput jsonOutput = new JsonOutput()
|
||||
Endpoint endpoint = null;
|
||||
try {
|
||||
@@ -95,33 +103,7 @@ class ResultsSender {
|
||||
me.write(os)
|
||||
os.writeShort((short)results.length)
|
||||
results.each {
|
||||
byte [] name = it.getFile().getName().getBytes(StandardCharsets.UTF_8)
|
||||
def baos = new ByteArrayOutputStream()
|
||||
def daos = new DataOutputStream(baos)
|
||||
daos.writeShort((short) name.length)
|
||||
daos.write(name)
|
||||
daos.flush()
|
||||
String encodedName = Base64.encode(baos.toByteArray())
|
||||
def obj = [:]
|
||||
obj.type = "Result"
|
||||
obj.version = oobInfohash ? 2 : 1
|
||||
obj.name = encodedName
|
||||
obj.infohash = Base64.encode(it.getInfoHash().getRoot())
|
||||
obj.size = it.getFile().length()
|
||||
obj.pieceSize = it.getPieceSize()
|
||||
if (!oobInfohash) {
|
||||
byte [] hashList = it.getInfoHash().getHashList()
|
||||
def hashListB64 = []
|
||||
for (int i = 0; i < hashList.length / InfoHash.SIZE; i++) {
|
||||
System.arraycopy(hashList, InfoHash.SIZE * i, tmp, 0, InfoHash.SIZE)
|
||||
hashListB64 << Base64.encode(tmp)
|
||||
}
|
||||
obj.hashList = hashListB64
|
||||
}
|
||||
|
||||
if (it instanceof DownloadedFile)
|
||||
obj.sources = it.sources.stream().map({dest -> dest.toBase64()}).collect(Collectors.toSet())
|
||||
|
||||
def obj = sharedFileToObj(it, settings.browseFiles)
|
||||
def json = jsonOutput.toJson(obj)
|
||||
os.writeShort((short)json.length())
|
||||
os.write(json.getBytes(StandardCharsets.US_ASCII))
|
||||
@@ -135,4 +117,30 @@ class ResultsSender {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static def sharedFileToObj(SharedFile sf, boolean browseFiles) {
|
||||
byte [] name = sf.getFile().getName().getBytes(StandardCharsets.UTF_8)
|
||||
def baos = new ByteArrayOutputStream()
|
||||
def daos = new DataOutputStream(baos)
|
||||
daos.writeShort((short) name.length)
|
||||
daos.write(name)
|
||||
daos.flush()
|
||||
String encodedName = Base64.encode(baos.toByteArray())
|
||||
def obj = [:]
|
||||
obj.type = "Result"
|
||||
obj.version = 2
|
||||
obj.name = encodedName
|
||||
obj.infohash = Base64.encode(sf.getInfoHash().getRoot())
|
||||
obj.size = sf.getCachedLength()
|
||||
obj.pieceSize = sf.getPieceSize()
|
||||
|
||||
if (sf instanceof DownloadedFile)
|
||||
obj.sources = sf.sources.stream().map({dest -> dest.toBase64()}).collect(Collectors.toSet())
|
||||
|
||||
if (sf.getComment() != null)
|
||||
obj.comment = sf.getComment()
|
||||
|
||||
obj.browse = browseFiles
|
||||
obj
|
||||
}
|
||||
}
|
||||
|
@@ -9,11 +9,12 @@ class SearchEvent extends Event {
|
||||
byte [] searchHash
|
||||
UUID uuid
|
||||
boolean oobInfohash
|
||||
boolean searchComments
|
||||
|
||||
String toString() {
|
||||
def infoHash = null
|
||||
if (searchHash != null)
|
||||
infoHash = new InfoHash(searchHash)
|
||||
"searchTerms: $searchTerms searchHash:$infoHash, uuid:$uuid oobInfohash:$oobInfohash"
|
||||
"searchTerms: $searchTerms searchHash:$infoHash, uuid:$uuid oobInfohash:$oobInfohash searchComments:$searchComments"
|
||||
}
|
||||
}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
package com.muwire.core.search
|
||||
|
||||
import com.muwire.core.Constants
|
||||
import com.muwire.core.SplitPattern
|
||||
|
||||
class SearchIndex {
|
||||
|
||||
@@ -32,7 +32,7 @@ class SearchIndex {
|
||||
}
|
||||
|
||||
private static String[] split(String source) {
|
||||
source = source.replaceAll(Constants.SPLIT_PATTERN, " ").toLowerCase()
|
||||
source = source.replaceAll(SplitPattern.SPLIT_PATTERN, " ").toLowerCase()
|
||||
String [] split = source.split(" ")
|
||||
def rv = []
|
||||
split.each { if (it.length() > 0) rv << it }
|
||||
|
@@ -0,0 +1,8 @@
|
||||
package com.muwire.core.search
|
||||
|
||||
import com.muwire.core.Event
|
||||
import com.muwire.core.Persona
|
||||
|
||||
class UIBrowseEvent extends Event {
|
||||
Persona host
|
||||
}
|
@@ -14,6 +14,8 @@ class UIResultEvent extends Event {
|
||||
long size
|
||||
InfoHash infohash
|
||||
int pieceSize
|
||||
String comment
|
||||
boolean browse
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
|
@@ -0,0 +1,31 @@
|
||||
package com.muwire.core.trust
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
import com.muwire.core.Persona
|
||||
|
||||
import net.i2p.util.ConcurrentHashSet
|
||||
|
||||
class RemoteTrustList {
|
||||
public enum Status { NEW, UPDATING, UPDATED, UPDATE_FAILED }
|
||||
|
||||
private final Persona persona
|
||||
private final Set<Persona> good, bad
|
||||
volatile long timestamp
|
||||
volatile boolean forceUpdate
|
||||
Status status = Status.NEW
|
||||
|
||||
RemoteTrustList(Persona persona) {
|
||||
this.persona = persona
|
||||
good = new ConcurrentHashSet<>()
|
||||
bad = new ConcurrentHashSet<>()
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (!(o instanceof RemoteTrustList))
|
||||
return false
|
||||
RemoteTrustList other = (RemoteTrustList)o
|
||||
persona == other.persona
|
||||
}
|
||||
}
|
@@ -0,0 +1,161 @@
|
||||
package com.muwire.core.trust
|
||||
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.logging.Level
|
||||
|
||||
import com.muwire.core.EventBus
|
||||
import com.muwire.core.MuWireSettings
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.UILoadedEvent
|
||||
import com.muwire.core.connection.Endpoint
|
||||
import com.muwire.core.connection.I2PConnector
|
||||
import com.muwire.core.util.DataUtil
|
||||
|
||||
import groovy.util.logging.Log
|
||||
import net.i2p.data.Destination
|
||||
|
||||
@Log
|
||||
class TrustSubscriber {
|
||||
private final EventBus eventBus
|
||||
private final I2PConnector i2pConnector
|
||||
private final MuWireSettings settings
|
||||
|
||||
private final Map<Destination, RemoteTrustList> remoteTrustLists = new ConcurrentHashMap<>()
|
||||
|
||||
private final Object waitLock = new Object()
|
||||
private volatile boolean shutdown
|
||||
private volatile Thread thread
|
||||
private final ExecutorService updateThreads = Executors.newCachedThreadPool()
|
||||
|
||||
TrustSubscriber(EventBus eventBus, I2PConnector i2pConnector, MuWireSettings settings) {
|
||||
this.eventBus = eventBus
|
||||
this.i2pConnector = i2pConnector
|
||||
this.settings = settings
|
||||
}
|
||||
|
||||
void onUILoadedEvent(UILoadedEvent e) {
|
||||
thread = new Thread({checkLoop()} as Runnable, "trust-subscriber")
|
||||
thread.setDaemon(true)
|
||||
thread.start()
|
||||
}
|
||||
|
||||
void stop() {
|
||||
shutdown = true
|
||||
thread?.interrupt()
|
||||
updateThreads.shutdownNow()
|
||||
}
|
||||
|
||||
void onTrustSubscriptionEvent(TrustSubscriptionEvent e) {
|
||||
if (!e.subscribe) {
|
||||
remoteTrustLists.remove(e.persona.destination)
|
||||
} else {
|
||||
RemoteTrustList trustList = remoteTrustLists.putIfAbsent(e.persona.destination, new RemoteTrustList(e.persona))
|
||||
trustList?.forceUpdate = true
|
||||
synchronized(waitLock) {
|
||||
waitLock.notify()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void checkLoop() {
|
||||
try {
|
||||
while(!shutdown) {
|
||||
synchronized(waitLock) {
|
||||
waitLock.wait(60 * 1000)
|
||||
}
|
||||
final long now = System.currentTimeMillis()
|
||||
remoteTrustLists.values().each { trustList ->
|
||||
if (trustList.status == RemoteTrustList.Status.UPDATING)
|
||||
return
|
||||
if (!trustList.forceUpdate &&
|
||||
now - trustList.timestamp < settings.trustListInterval * 60 * 60 * 1000)
|
||||
return
|
||||
trustList.forceUpdate = false
|
||||
updateThreads.submit(new UpdateJob(trustList))
|
||||
}
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
if (!shutdown)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
private class UpdateJob implements Runnable {
|
||||
|
||||
private final RemoteTrustList trustList
|
||||
|
||||
UpdateJob(RemoteTrustList trustList) {
|
||||
this.trustList = trustList
|
||||
}
|
||||
|
||||
public void run() {
|
||||
trustList.status = RemoteTrustList.Status.UPDATING
|
||||
eventBus.publish(new TrustSubscriptionUpdatedEvent(trustList : trustList))
|
||||
if (check(trustList, System.currentTimeMillis()))
|
||||
trustList.status = RemoteTrustList.Status.UPDATED
|
||||
else
|
||||
trustList.status = RemoteTrustList.Status.UPDATE_FAILED
|
||||
eventBus.publish(new TrustSubscriptionUpdatedEvent(trustList : trustList))
|
||||
}
|
||||
}
|
||||
|
||||
private boolean check(RemoteTrustList trustList, long now) {
|
||||
log.info("fetching trust list from ${trustList.persona.getHumanReadableName()}")
|
||||
Endpoint endpoint = null
|
||||
try {
|
||||
endpoint = i2pConnector.connect(trustList.persona.destination)
|
||||
OutputStream os = endpoint.getOutputStream()
|
||||
InputStream is = endpoint.getInputStream()
|
||||
os.write("TRUST\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
os.flush()
|
||||
|
||||
String codeString = DataUtil.readTillRN(is)
|
||||
int space = codeString.indexOf(' ')
|
||||
if (space > 0)
|
||||
codeString = codeString.substring(0,space)
|
||||
int code = Integer.parseInt(codeString.trim())
|
||||
|
||||
if (code != 200) {
|
||||
log.info("couldn't fetch trust list, code $code")
|
||||
return false
|
||||
}
|
||||
|
||||
// swallow any headers
|
||||
String header
|
||||
while (( header = DataUtil.readTillRN(is)) != "");
|
||||
|
||||
DataInputStream dis = new DataInputStream(is)
|
||||
|
||||
Set<Persona> good = new HashSet<>()
|
||||
int nGood = dis.readUnsignedShort()
|
||||
for (int i = 0; i < nGood; i++) {
|
||||
Persona p = new Persona(dis)
|
||||
good.add(p)
|
||||
}
|
||||
|
||||
Set<Persona> bad = new HashSet<>()
|
||||
int nBad = dis.readUnsignedShort()
|
||||
for (int i = 0; i < nBad; i++) {
|
||||
Persona p = new Persona(dis)
|
||||
bad.add(p)
|
||||
}
|
||||
|
||||
trustList.timestamp = now
|
||||
trustList.good.clear()
|
||||
trustList.good.addAll(good)
|
||||
trustList.bad.clear()
|
||||
trustList.bad.addAll(bad)
|
||||
|
||||
return true
|
||||
} catch (Exception e) {
|
||||
log.log(Level.WARNING,"exception fetching trust list from ${trustList.persona.getHumanReadableName()}",e)
|
||||
return false
|
||||
} finally {
|
||||
endpoint?.close()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@@ -0,0 +1,9 @@
|
||||
package com.muwire.core.trust
|
||||
|
||||
import com.muwire.core.Event
|
||||
import com.muwire.core.Persona
|
||||
|
||||
class TrustSubscriptionEvent extends Event {
|
||||
Persona persona
|
||||
boolean subscribe
|
||||
}
|
@@ -0,0 +1,7 @@
|
||||
package com.muwire.core.trust
|
||||
|
||||
import com.muwire.core.Event
|
||||
|
||||
class TrustSubscriptionUpdatedEvent extends Event {
|
||||
RemoteTrustList trustList
|
||||
}
|
@@ -3,5 +3,5 @@ package com.muwire.core.update
|
||||
import net.i2p.data.Destination
|
||||
|
||||
class UpdateServers {
|
||||
static final Destination UPDATE_SERVER = new Destination("pSWieSRB3czCl3Zz4WpKp4Z8tjv-05zbogRDS7SEnKcSdWOupVwjzQ92GsgQh1VqgoSRk1F8dpZOnHxxz5HFy9D7ri0uFdkMyXdSKoB7IgkkvCfTAyEmeaPwSYnurF3Zk7u286E7YG2rZkQZgJ77tow7ZS0mxFB7Z0Ti-VkZ9~GeGePW~howwNm4iSQACZA0DyTpI8iv5j4I0itPCQRgaGziob~Vfvjk49nd8N4jtaDGo9cEcafikVzQ2OgBgYWL6LRbrrItwuGqsDvITUHWaElUYIDhRQYUq8gYiUA6rwAJputfhFU0J7lIxFR9vVY7YzRvcFckfr0DNI4VQVVlPnRPkUxQa--BlldMaCIppWugjgKLwqiSiHywKpSMlBWgY2z1ry4ueEBo1WEP-mEf88wRk4cFQBCKtctCQnIG2GsnATqTl-VGUAsuzeNWZiFSwXiTy~gQ094yWx-K06fFZUDt4CMiLZVhGlixiInD~34FCRC9LVMtFcqiFB2M-Ql2AAAA")
|
||||
static final Destination UPDATE_SERVER = new Destination("VJYAiCPZHNLraWvLkeRLxRiT4PHAqNqRO1nH240r7u1noBw8Pa~-lJOhKR7CccPkEN8ejSi4H6XjqKYLC8BKLVLeOgnAbedUVx81MV7DETPDdPEGV4RVu6YDFri7-tJOeqauGHxtlXT44YWuR69xKrTG3u4~iTWgxKnlBDht9Q3aVpSPFD2KqEizfVxolqXI0zmAZ2xMi8jfl0oe4GbgHrD9hR2FYj6yKfdqcUgHVobY4kDdJt-u31QqwWdsQMEj8Y3tR2XcNaITEVPiAjoKgBrYwB4jddWPNaT4XdHz76d9p9Iqes7dhOKq3OKpk6kg-bfIKiEOiA1mY49fn5h8pNShTqV7QBhh4CE4EDT3Szl~WsLdrlHUKJufSi7erEMh3coF7HORpF1wah2Xw7q470t~b8dKGKi7N7xQsqhGruDm66PH9oE9Kt9WBVBq2zORdPRtRM61I7EnrwDlbOkL0y~XpvQ3JKUQKdBQ3QsOJt8CHlhHHXMMbvqhntR61RSDBQAEAAcAAA==")
|
||||
}
|
||||
|
@@ -83,7 +83,7 @@ class ContentUploader extends Uploader {
|
||||
String xHave = DataUtil.encodeXHave(mesh.pieces.getDownloaded(), mesh.pieces.nPieces)
|
||||
endpoint.getOutputStream().write("X-Have: $xHave\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
|
||||
Set<Persona> sources = mesh.getRandom(3, toExclude)
|
||||
Set<Persona> sources = mesh.getRandom(9, toExclude)
|
||||
if (!sources.isEmpty()) {
|
||||
String xAlts = sources.stream().map({ it.toBase64() }).collect(Collectors.joining(","))
|
||||
endpoint.getOutputStream().write("X-Alt: $xAlts\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
@@ -119,4 +119,8 @@ class ContentUploader extends Uploader {
|
||||
return mesh.pieces.nPieces;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getTotalSize() {
|
||||
return file.length();
|
||||
}
|
||||
}
|
||||
|
@@ -61,5 +61,8 @@ class HashListUploader extends Uploader {
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public long getTotalSize() {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
@@ -92,7 +92,7 @@ public class UploadManager {
|
||||
pieceSize = downloader.pieceSizePow2
|
||||
} else {
|
||||
SharedFile sharedFile = sharedFiles.iterator().next();
|
||||
mesh = meshManager.getOrCreate(request.infoHash, sharedFile.NPieces)
|
||||
mesh = meshManager.getOrCreate(request.infoHash, sharedFile.NPieces, false)
|
||||
file = sharedFile.file
|
||||
pieceSize = sharedFile.pieceSize
|
||||
}
|
||||
@@ -217,7 +217,7 @@ public class UploadManager {
|
||||
pieceSize = downloader.pieceSizePow2
|
||||
} else {
|
||||
SharedFile sharedFile = sharedFiles.iterator().next();
|
||||
mesh = meshManager.getOrCreate(request.infoHash, sharedFile.NPieces)
|
||||
mesh = meshManager.getOrCreate(request.infoHash, sharedFile.NPieces, false)
|
||||
file = sharedFile.file
|
||||
pieceSize = sharedFile.pieceSize
|
||||
}
|
||||
|
@@ -35,5 +35,7 @@ abstract class Uploader {
|
||||
|
||||
abstract int getDonePieces();
|
||||
|
||||
abstract int getTotalPieces()
|
||||
abstract int getTotalPieces();
|
||||
|
||||
abstract long getTotalSize();
|
||||
}
|
||||
|
@@ -1,156 +0,0 @@
|
||||
package com.muwire.core.util
|
||||
|
||||
import java.lang.reflect.Field
|
||||
import java.lang.reflect.Method
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.charset.StandardCharsets
|
||||
|
||||
import com.muwire.core.Constants
|
||||
|
||||
import net.i2p.data.Base64
|
||||
|
||||
class DataUtil {
|
||||
|
||||
private final static int MAX_SHORT = (0x1 << 16) - 1
|
||||
|
||||
static void writeUnsignedShort(int value, OutputStream os) {
|
||||
if (value > MAX_SHORT || value < 0)
|
||||
throw new IllegalArgumentException("$value invalid")
|
||||
|
||||
byte lsb = (byte) (value & 0xFF)
|
||||
byte msb = (byte) (value >> 8)
|
||||
|
||||
os.write(msb)
|
||||
os.write(lsb)
|
||||
}
|
||||
|
||||
private final static int MAX_HEADER = 0x7FFFFF
|
||||
|
||||
static void packHeader(int length, byte [] header) {
|
||||
if (header.length != 3)
|
||||
throw new IllegalArgumentException("header length $header.length")
|
||||
if (length < 0 || length > MAX_HEADER)
|
||||
throw new IllegalArgumentException("length $length")
|
||||
|
||||
header[2] = (byte) (length & 0xFF)
|
||||
header[1] = (byte) ((length >> 8) & 0xFF)
|
||||
header[0] = (byte) ((length >> 16) & 0x7F)
|
||||
}
|
||||
|
||||
static int readLength(byte [] header) {
|
||||
if (header.length != 3)
|
||||
throw new IllegalArgumentException("header length $header.length")
|
||||
|
||||
return (((int)(header[0] & 0x7F)) << 16) |
|
||||
(((int)(header[1] & 0xFF) << 8)) |
|
||||
((int)header[2] & 0xFF)
|
||||
}
|
||||
|
||||
static String readi18nString(byte [] encoded) {
|
||||
if (encoded.length < 2)
|
||||
throw new IllegalArgumentException("encoding too short $encoded.length")
|
||||
int length = ((encoded[0] & 0xFF) << 8) | (encoded[1] & 0xFF)
|
||||
if (encoded.length != length + 2)
|
||||
throw new IllegalArgumentException("encoding doesn't match length, expected $length found $encoded.length")
|
||||
byte [] string = new byte[length]
|
||||
System.arraycopy(encoded, 2, string, 0, length)
|
||||
new String(string, StandardCharsets.UTF_8)
|
||||
}
|
||||
|
||||
static byte[] encodei18nString(String string) {
|
||||
byte [] utf8 = string.getBytes(StandardCharsets.UTF_8)
|
||||
if (utf8.length > Short.MAX_VALUE)
|
||||
throw new IllegalArgumentException("String in utf8 too long $utf8.length")
|
||||
def baos = new ByteArrayOutputStream()
|
||||
def daos = new DataOutputStream(baos)
|
||||
daos.writeShort((short) utf8.length)
|
||||
daos.write(utf8)
|
||||
daos.close()
|
||||
baos.toByteArray()
|
||||
}
|
||||
|
||||
public static String readTillRN(InputStream is) {
|
||||
def baos = new ByteArrayOutputStream()
|
||||
while(baos.size() < (Constants.MAX_HEADER_SIZE)) {
|
||||
byte read = is.read()
|
||||
if (read == -1)
|
||||
throw new IOException()
|
||||
if (read == '\r') {
|
||||
if (is.read() != '\n')
|
||||
throw new IOException("invalid header")
|
||||
break
|
||||
}
|
||||
baos.write(read)
|
||||
}
|
||||
new String(baos.toByteArray(), StandardCharsets.US_ASCII)
|
||||
}
|
||||
|
||||
public static String encodeXHave(List<Integer> pieces, int totalPieces) {
|
||||
int bytes = totalPieces / 8
|
||||
if (totalPieces % 8 != 0)
|
||||
bytes++
|
||||
byte[] raw = new byte[bytes]
|
||||
pieces.each {
|
||||
int byteIdx = it / 8
|
||||
int offset = it % 8
|
||||
int mask = 0x80 >>> offset
|
||||
raw[byteIdx] |= mask
|
||||
}
|
||||
Base64.encode(raw)
|
||||
}
|
||||
|
||||
public static List<Integer> decodeXHave(String xHave) {
|
||||
byte [] availablePieces = Base64.decode(xHave)
|
||||
List<Integer> available = new ArrayList<>()
|
||||
availablePieces.eachWithIndex {b, i ->
|
||||
for (int j = 0; j < 8 ; j++) {
|
||||
byte mask = 0x80 >>> j
|
||||
if ((b & mask) == mask) {
|
||||
available.add(i * 8 + j)
|
||||
}
|
||||
}
|
||||
}
|
||||
available
|
||||
}
|
||||
|
||||
public static Exception findRoot(Exception e) {
|
||||
while(e.getCause() != null)
|
||||
e = e.getCause()
|
||||
e
|
||||
}
|
||||
|
||||
public static void tryUnmap(ByteBuffer cb) {
|
||||
if (cb==null || !cb.isDirect()) return;
|
||||
// we could use this type cast and call functions without reflection code,
|
||||
// but static import from sun.* package is risky for non-SUN virtual machine.
|
||||
//try { ((sun.nio.ch.DirectBuffer)cb).cleaner().clean(); } catch (Exception ex) { }
|
||||
|
||||
// JavaSpecVer: 1.6, 1.7, 1.8, 9, 10
|
||||
boolean isOldJDK = System.getProperty("java.specification.version","99").startsWith("1.");
|
||||
try {
|
||||
if (isOldJDK) {
|
||||
Method cleaner = cb.getClass().getMethod("cleaner");
|
||||
cleaner.setAccessible(true);
|
||||
Method clean = Class.forName("sun.misc.Cleaner").getMethod("clean");
|
||||
clean.setAccessible(true);
|
||||
clean.invoke(cleaner.invoke(cb));
|
||||
} else {
|
||||
Class unsafeClass;
|
||||
try {
|
||||
unsafeClass = Class.forName("sun.misc.Unsafe");
|
||||
} catch(Exception ex) {
|
||||
// jdk.internal.misc.Unsafe doesn't yet have an invokeCleaner() method,
|
||||
// but that method should be added if sun.misc.Unsafe is removed.
|
||||
unsafeClass = Class.forName("jdk.internal.misc.Unsafe");
|
||||
}
|
||||
Method clean = unsafeClass.getMethod("invokeCleaner", ByteBuffer.class);
|
||||
clean.setAccessible(true);
|
||||
Field theUnsafeField = unsafeClass.getDeclaredField("theUnsafe");
|
||||
theUnsafeField.setAccessible(true);
|
||||
Object theUnsafe = theUnsafeField.get(null);
|
||||
clean.invoke(theUnsafe, cb);
|
||||
}
|
||||
} catch(Exception ex) { }
|
||||
cb = null;
|
||||
}
|
||||
}
|
11
core/src/main/java/com/muwire/core/Constants.java
Normal file
11
core/src/main/java/com/muwire/core/Constants.java
Normal file
@@ -0,0 +1,11 @@
|
||||
package com.muwire.core;
|
||||
|
||||
import net.i2p.crypto.SigType;
|
||||
|
||||
public class Constants {
|
||||
public static final byte PERSONA_VERSION = (byte)1;
|
||||
public static final SigType SIG_TYPE = SigType.EdDSA_SHA512_Ed25519;
|
||||
|
||||
public static final int MAX_HEADER_SIZE = 0x1 << 14;
|
||||
public static final int MAX_HEADERS = 16;
|
||||
}
|
@@ -2,6 +2,12 @@ package com.muwire.core;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import com.muwire.core.util.DataUtil;
|
||||
|
||||
import net.i2p.data.Base64;
|
||||
|
||||
public class SharedFile {
|
||||
|
||||
@@ -12,12 +18,28 @@ public class SharedFile {
|
||||
private final String cachedPath;
|
||||
private final long cachedLength;
|
||||
|
||||
private final String b64EncodedFileName;
|
||||
private final String b64EncodedHashRoot;
|
||||
private final List<String> b64EncodedHashList;
|
||||
|
||||
private volatile String comment;
|
||||
|
||||
public SharedFile(File file, InfoHash infoHash, int pieceSize) throws IOException {
|
||||
this.file = file;
|
||||
this.infoHash = infoHash;
|
||||
this.pieceSize = pieceSize;
|
||||
this.cachedPath = file.getAbsolutePath();
|
||||
this.cachedLength = file.length();
|
||||
this.b64EncodedFileName = Base64.encode(DataUtil.encodei18nString(file.toString()));
|
||||
this.b64EncodedHashRoot = Base64.encode(infoHash.getRoot());
|
||||
|
||||
List<String> b64List = new ArrayList<String>();
|
||||
byte[] tmp = new byte[32];
|
||||
for (int i = 0; i < infoHash.getHashList().length / 32; i++) {
|
||||
System.arraycopy(infoHash.getHashList(), i * 32, tmp, 0, 32);
|
||||
b64List.add(Base64.encode(tmp));
|
||||
}
|
||||
this.b64EncodedHashList = b64List;
|
||||
}
|
||||
|
||||
public File getFile() {
|
||||
@@ -41,6 +63,18 @@ public class SharedFile {
|
||||
return rv;
|
||||
}
|
||||
|
||||
public String getB64EncodedFileName() {
|
||||
return b64EncodedFileName;
|
||||
}
|
||||
|
||||
public String getB64EncodedHashRoot() {
|
||||
return b64EncodedHashRoot;
|
||||
}
|
||||
|
||||
public List<String> getB64EncodedHashList() {
|
||||
return b64EncodedHashList;
|
||||
}
|
||||
|
||||
public String getCachedPath() {
|
||||
return cachedPath;
|
||||
}
|
||||
@@ -49,6 +83,14 @@ public class SharedFile {
|
||||
return cachedLength;
|
||||
}
|
||||
|
||||
public void setComment(String comment) {
|
||||
this.comment = comment;
|
||||
}
|
||||
|
||||
public String getComment() {
|
||||
return comment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return file.hashCode() ^ infoHash.hashCode();
|
||||
|
168
core/src/main/java/com/muwire/core/util/DataUtil.java
Normal file
168
core/src/main/java/com/muwire/core/util/DataUtil.java
Normal file
@@ -0,0 +1,168 @@
|
||||
package com.muwire.core.util;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import com.muwire.core.Constants;
|
||||
|
||||
import net.i2p.data.Base64;
|
||||
|
||||
public class DataUtil {
|
||||
|
||||
private final static int MAX_SHORT = (0x1 << 16) - 1;
|
||||
|
||||
static void writeUnsignedShort(int value, OutputStream os) throws IOException {
|
||||
if (value > MAX_SHORT || value < 0)
|
||||
throw new IllegalArgumentException("$value invalid");
|
||||
|
||||
byte lsb = (byte) (value & 0xFF);
|
||||
byte msb = (byte) (value >> 8);
|
||||
|
||||
os.write(msb);
|
||||
os.write(lsb);
|
||||
}
|
||||
|
||||
private final static int MAX_HEADER = 0x7FFFFF;
|
||||
|
||||
static void packHeader(int length, byte [] header) {
|
||||
if (header.length != 3)
|
||||
throw new IllegalArgumentException("header length $header.length");
|
||||
if (length < 0 || length > MAX_HEADER)
|
||||
throw new IllegalArgumentException("length $length");
|
||||
|
||||
header[2] = (byte) (length & 0xFF);
|
||||
header[1] = (byte) ((length >> 8) & 0xFF);
|
||||
header[0] = (byte) ((length >> 16) & 0x7F);
|
||||
}
|
||||
|
||||
static int readLength(byte [] header) {
|
||||
if (header.length != 3)
|
||||
throw new IllegalArgumentException("header length $header.length");
|
||||
|
||||
return (((int)(header[0] & 0x7F)) << 16) |
|
||||
(((int)(header[1] & 0xFF) << 8)) |
|
||||
((int)header[2] & 0xFF);
|
||||
}
|
||||
|
||||
static String readi18nString(byte [] encoded) {
|
||||
if (encoded.length < 2)
|
||||
throw new IllegalArgumentException("encoding too short $encoded.length");
|
||||
int length = ((encoded[0] & 0xFF) << 8) | (encoded[1] & 0xFF);
|
||||
if (encoded.length != length + 2)
|
||||
throw new IllegalArgumentException("encoding doesn't match length, expected $length found $encoded.length");
|
||||
byte [] string = new byte[length];
|
||||
System.arraycopy(encoded, 2, string, 0, length);
|
||||
return new String(string, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
public static byte[] encodei18nString(String string) {
|
||||
byte [] utf8 = string.getBytes(StandardCharsets.UTF_8);
|
||||
if (utf8.length > Short.MAX_VALUE)
|
||||
throw new IllegalArgumentException("String in utf8 too long $utf8.length");
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
DataOutputStream daos = new DataOutputStream(baos);
|
||||
try {
|
||||
daos.writeShort((short) utf8.length);
|
||||
daos.write(utf8);
|
||||
daos.close();
|
||||
} catch (IOException impossible) {
|
||||
throw new IllegalStateException(impossible);
|
||||
}
|
||||
return baos.toByteArray();
|
||||
}
|
||||
|
||||
public static String readTillRN(InputStream is) throws IOException {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
while(baos.size() < (Constants.MAX_HEADER_SIZE)) {
|
||||
int read = is.read();
|
||||
if (read == -1)
|
||||
throw new IOException();
|
||||
if (read == '\r') {
|
||||
if (is.read() != '\n')
|
||||
throw new IOException("invalid header");
|
||||
break;
|
||||
}
|
||||
baos.write(read);
|
||||
}
|
||||
return new String(baos.toByteArray(), StandardCharsets.US_ASCII);
|
||||
}
|
||||
|
||||
public static String encodeXHave(List<Integer> pieces, int totalPieces) {
|
||||
int bytes = totalPieces / 8;
|
||||
if (totalPieces % 8 != 0)
|
||||
bytes++;
|
||||
byte[] raw = new byte[bytes];
|
||||
for (int it : pieces) {
|
||||
int byteIdx = it / 8;
|
||||
int offset = it % 8;
|
||||
int mask = 0x80 >>> offset;
|
||||
raw[byteIdx] |= mask;
|
||||
}
|
||||
return Base64.encode(raw);
|
||||
}
|
||||
|
||||
public static List<Integer> decodeXHave(String xHave) {
|
||||
byte [] availablePieces = Base64.decode(xHave);
|
||||
List<Integer> available = new ArrayList<>();
|
||||
for (int i = 0; i < availablePieces.length; i ++) {
|
||||
byte b = availablePieces[i];
|
||||
for (int j = 0; j < 8 ; j++) {
|
||||
byte mask = (byte) (0x80 >>> j);
|
||||
if ((b & mask) == mask) {
|
||||
available.add(i * 8 + j);
|
||||
}
|
||||
}
|
||||
}
|
||||
return available;
|
||||
}
|
||||
|
||||
public static Throwable findRoot(Throwable e) {
|
||||
while(e.getCause() != null)
|
||||
e = e.getCause();
|
||||
return e;
|
||||
}
|
||||
|
||||
public static void tryUnmap(ByteBuffer cb) {
|
||||
if (cb==null || !cb.isDirect()) return;
|
||||
// we could use this type cast and call functions without reflection code,
|
||||
// but static import from sun.* package is risky for non-SUN virtual machine.
|
||||
//try { ((sun.nio.ch.DirectBuffer)cb).cleaner().clean(); } catch (Exception ex) { }
|
||||
|
||||
// JavaSpecVer: 1.6, 1.7, 1.8, 9, 10
|
||||
boolean isOldJDK = System.getProperty("java.specification.version","99").startsWith("1.");
|
||||
try {
|
||||
if (isOldJDK) {
|
||||
Method cleaner = cb.getClass().getMethod("cleaner");
|
||||
cleaner.setAccessible(true);
|
||||
Method clean = Class.forName("sun.misc.Cleaner").getMethod("clean");
|
||||
clean.setAccessible(true);
|
||||
clean.invoke(cleaner.invoke(cb));
|
||||
} else {
|
||||
Class unsafeClass;
|
||||
try {
|
||||
unsafeClass = Class.forName("sun.misc.Unsafe");
|
||||
} catch(Exception ex) {
|
||||
// jdk.internal.misc.Unsafe doesn't yet have an invokeCleaner() method,
|
||||
// but that method should be added if sun.misc.Unsafe is removed.
|
||||
unsafeClass = Class.forName("jdk.internal.misc.Unsafe");
|
||||
}
|
||||
Method clean = unsafeClass.getMethod("invokeCleaner", ByteBuffer.class);
|
||||
clean.setAccessible(true);
|
||||
Field theUnsafeField = unsafeClass.getDeclaredField("theUnsafe");
|
||||
theUnsafeField.setAccessible(true);
|
||||
Object theUnsafe = theUnsafeField.get(null);
|
||||
clean.invoke(theUnsafe, cb);
|
||||
}
|
||||
} catch(Exception ex) { }
|
||||
cb = null;
|
||||
}
|
||||
}
|
@@ -4,6 +4,7 @@ import static org.junit.Assert.fail
|
||||
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Ignore
|
||||
import org.junit.Test
|
||||
|
||||
import com.muwire.core.EventBus
|
||||
@@ -180,10 +181,11 @@ class DownloadSessionTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore // this needs to be rewritten with stealing in mind
|
||||
public void testSmallFileClaimed() {
|
||||
initSession(20, [0])
|
||||
long now = System.currentTimeMillis()
|
||||
downloadThread.join(100)
|
||||
downloadThread.join(150)
|
||||
assert 100 >= (System.currentTimeMillis() - now)
|
||||
assert !performed
|
||||
assert available.isEmpty()
|
||||
|
@@ -16,7 +16,7 @@ class PiecesTest {
|
||||
public void testSinglePiece() {
|
||||
pieces = new Pieces(1)
|
||||
assert !pieces.isComplete()
|
||||
assert pieces.claim() == 0
|
||||
assert pieces.claim() == [0,0,0]
|
||||
pieces.markDownloaded(0)
|
||||
assert pieces.isComplete()
|
||||
}
|
||||
@@ -25,28 +25,28 @@ class PiecesTest {
|
||||
public void testTwoPieces() {
|
||||
pieces = new Pieces(2)
|
||||
assert !pieces.isComplete()
|
||||
int piece = pieces.claim()
|
||||
assert piece == 0 || piece == 1
|
||||
pieces.markDownloaded(piece)
|
||||
int[] piece = pieces.claim()
|
||||
assert piece[0] == 0 || piece[0] == 1
|
||||
pieces.markDownloaded(piece[0])
|
||||
assert !pieces.isComplete()
|
||||
int piece2 = pieces.claim()
|
||||
assert piece != piece2
|
||||
pieces.markDownloaded(piece2)
|
||||
int[] piece2 = pieces.claim()
|
||||
assert piece[0] != piece2[0]
|
||||
pieces.markDownloaded(piece2[0])
|
||||
assert pieces.isComplete()
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testClaimAvailable() {
|
||||
pieces = new Pieces(2)
|
||||
int claimed = pieces.claim([0].toSet())
|
||||
assert claimed == 0
|
||||
assert -1 == pieces.claim([0].toSet())
|
||||
int[] claimed = pieces.claim([0].toSet())
|
||||
assert claimed == [0,0,0]
|
||||
assert [0,0,1] == pieces.claim([0].toSet())
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testClaimNoneAvailable() {
|
||||
pieces = new Pieces(20)
|
||||
int claimed = pieces.claim()
|
||||
assert -1 == pieces.claim([claimed].toSet())
|
||||
int[] claimed = pieces.claim()
|
||||
assert [0,0,0] == pieces.claim(claimed.toSet())
|
||||
}
|
||||
}
|
||||
|
@@ -25,7 +25,8 @@ class HasherServiceTest {
|
||||
void before() {
|
||||
eventBus = new EventBus()
|
||||
hasher = new FileHasher()
|
||||
service = new HasherService(hasher, eventBus, new FileManager(eventBus, new MuWireSettings()))
|
||||
def props = new MuWireSettings()
|
||||
service = new HasherService(hasher, eventBus, new FileManager(eventBus, props), props)
|
||||
eventBus.register(FileHashedEvent.class, listener)
|
||||
eventBus.register(FileSharedEvent.class, service)
|
||||
service.start()
|
||||
|
@@ -72,6 +72,9 @@ class HostCacheTest {
|
||||
TrustLevel.NEUTRAL
|
||||
}
|
||||
settingsMock.ignore.allowUntrusted { true }
|
||||
settingsMock.ignore.getHostClearInterval { 0 }
|
||||
settingsMock.ignore.getHostHopelessInterval { 0 }
|
||||
settingsMock.ignore.getHostRejectInterval { 0 }
|
||||
|
||||
initMocks()
|
||||
|
||||
@@ -91,6 +94,10 @@ class HostCacheTest {
|
||||
TrustLevel.DISTRUSTED
|
||||
}
|
||||
|
||||
settingsMock.ignore.getHostClearInterval { 0 }
|
||||
settingsMock.ignore.getHostHopelessInterval { 0 }
|
||||
settingsMock.ignore.getHostRejectInterval { 0 }
|
||||
|
||||
initMocks()
|
||||
|
||||
cache.onHostDiscoveredEvent(new HostDiscoveredEvent(destination: destinations.dest1))
|
||||
@@ -104,6 +111,9 @@ class HostCacheTest {
|
||||
TrustLevel.NEUTRAL
|
||||
}
|
||||
settingsMock.ignore.allowUntrusted { false }
|
||||
settingsMock.ignore.getHostClearInterval { 0 }
|
||||
settingsMock.ignore.getHostHopelessInterval { 0 }
|
||||
settingsMock.ignore.getHostRejectInterval { 0 }
|
||||
|
||||
initMocks()
|
||||
|
||||
@@ -123,6 +133,9 @@ class HostCacheTest {
|
||||
}
|
||||
trustMock.demand.getLevel{ d -> TrustLevel.TRUSTED }
|
||||
trustMock.demand.getLevel{ d -> TrustLevel.TRUSTED }
|
||||
settingsMock.ignore.getHostClearInterval { 0 }
|
||||
settingsMock.ignore.getHostHopelessInterval { 0 }
|
||||
settingsMock.ignore.getHostRejectInterval { 0 }
|
||||
|
||||
initMocks()
|
||||
cache.onHostDiscoveredEvent(new HostDiscoveredEvent(destination: destinations.dest1))
|
||||
@@ -139,6 +152,14 @@ class HostCacheTest {
|
||||
assert d == destinations.dest1
|
||||
TrustLevel.TRUSTED
|
||||
}
|
||||
trustMock.demand.getLevel { d ->
|
||||
assert d == destinations.dest1
|
||||
TrustLevel.TRUSTED
|
||||
}
|
||||
|
||||
settingsMock.ignore.getHostClearInterval { 100 }
|
||||
settingsMock.ignore.getHostHopelessInterval { 0 }
|
||||
settingsMock.ignore.getHostRejectInterval { 0 }
|
||||
|
||||
initMocks()
|
||||
cache.onHostDiscoveredEvent(new HostDiscoveredEvent(destination: destinations.dest1))
|
||||
@@ -158,6 +179,10 @@ class HostCacheTest {
|
||||
TrustLevel.TRUSTED
|
||||
}
|
||||
|
||||
settingsMock.ignore.getHostClearInterval { 0 }
|
||||
settingsMock.ignore.getHostHopelessInterval { 0 }
|
||||
settingsMock.ignore.getHostRejectInterval { 0 }
|
||||
|
||||
initMocks()
|
||||
cache.onHostDiscoveredEvent(new HostDiscoveredEvent(destination: destinations.dest1))
|
||||
|
||||
@@ -183,6 +208,10 @@ class HostCacheTest {
|
||||
TrustLevel.TRUSTED
|
||||
}
|
||||
|
||||
settingsMock.ignore.getHostClearInterval { 0 }
|
||||
settingsMock.ignore.getHostHopelessInterval { 0 }
|
||||
settingsMock.ignore.getHostRejectInterval { 0 }
|
||||
|
||||
initMocks()
|
||||
cache.onHostDiscoveredEvent(new HostDiscoveredEvent(destination: destinations.dest1))
|
||||
|
||||
@@ -214,6 +243,10 @@ class HostCacheTest {
|
||||
TrustLevel.TRUSTED
|
||||
}
|
||||
|
||||
settingsMock.ignore.getHostClearInterval { 0 }
|
||||
settingsMock.ignore.getHostHopelessInterval { 0 }
|
||||
settingsMock.ignore.getHostRejectInterval { 0 }
|
||||
|
||||
initMocks()
|
||||
cache.onHostDiscoveredEvent(new HostDiscoveredEvent(destination: destinations.dest1))
|
||||
cache.onHostDiscoveredEvent(new HostDiscoveredEvent(destination: destinations.dest1))
|
||||
@@ -229,6 +262,11 @@ class HostCacheTest {
|
||||
assert d == destinations.dest1
|
||||
TrustLevel.TRUSTED
|
||||
}
|
||||
|
||||
settingsMock.ignore.getHostClearInterval { 0 }
|
||||
settingsMock.ignore.getHostHopelessInterval { 0 }
|
||||
settingsMock.ignore.getHostRejectInterval { 0 }
|
||||
|
||||
initMocks()
|
||||
cache.onHostDiscoveredEvent(new HostDiscoveredEvent(destination: destinations.dest1))
|
||||
Thread.sleep(150)
|
||||
@@ -260,6 +298,10 @@ class HostCacheTest {
|
||||
TrustLevel.TRUSTED
|
||||
}
|
||||
|
||||
settingsMock.ignore.getHostClearInterval { 0 }
|
||||
settingsMock.ignore.getHostHopelessInterval { 0 }
|
||||
settingsMock.ignore.getHostRejectInterval { 0 }
|
||||
|
||||
initMocks()
|
||||
def rv = cache.getHosts(5)
|
||||
assert rv.size() == 1
|
||||
|
@@ -9,6 +9,9 @@ import org.junit.Test
|
||||
|
||||
import com.muwire.core.InfoHash
|
||||
import com.muwire.core.connection.Endpoint
|
||||
import com.muwire.core.download.Pieces
|
||||
import com.muwire.core.files.FileHasher
|
||||
import com.muwire.core.mesh.Mesh
|
||||
|
||||
class UploaderTest {
|
||||
|
||||
@@ -52,7 +55,13 @@ class UploaderTest {
|
||||
}
|
||||
|
||||
private void startUpload() {
|
||||
uploader = new ContentUploader(file, request, endpoint)
|
||||
def hasher = new FileHasher()
|
||||
InfoHash infoHash = hasher.hashFile(file)
|
||||
Pieces pieces = new Pieces(FileHasher.getPieceSize(file.length()))
|
||||
for (int i = 0; i < pieces.nPieces; i++)
|
||||
pieces.markDownloaded(i)
|
||||
Mesh mesh = new Mesh(infoHash, pieces)
|
||||
uploader = new ContentUploader(file, request, endpoint, mesh, FileHasher.getPieceSize(file.length()))
|
||||
uploadThread = new Thread(uploader.respond() as Runnable)
|
||||
uploadThread.setDaemon(true)
|
||||
uploadThread.start()
|
||||
@@ -81,6 +90,7 @@ class UploaderTest {
|
||||
startUpload()
|
||||
assert "200 OK" == readUntilRN()
|
||||
assert "Content-Range: 0-19" == readUntilRN()
|
||||
assert readUntilRN().startsWith("X-Have")
|
||||
assert "" == readUntilRN()
|
||||
|
||||
byte [] data = new byte[20]
|
||||
@@ -96,6 +106,7 @@ class UploaderTest {
|
||||
startUpload()
|
||||
assert "200 OK" == readUntilRN()
|
||||
assert "Content-Range: 5-15" == readUntilRN()
|
||||
assert readUntilRN().startsWith("X-Have")
|
||||
assert "" == readUntilRN()
|
||||
|
||||
byte [] data = new byte[11]
|
||||
@@ -111,6 +122,7 @@ class UploaderTest {
|
||||
request = new ContentRequest(range : new Range(0,20))
|
||||
startUpload()
|
||||
assert "416 Range Not Satisfiable" == readUntilRN()
|
||||
assert readUntilRN().startsWith("X-Have")
|
||||
assert "" == readUntilRN()
|
||||
}
|
||||
|
||||
@@ -123,6 +135,7 @@ class UploaderTest {
|
||||
readUntilRN()
|
||||
readUntilRN()
|
||||
readUntilRN()
|
||||
readUntilRN()
|
||||
|
||||
byte [] data = new byte[length]
|
||||
DataInputStream dis = new DataInputStream(is)
|
||||
|
@@ -1,8 +1,20 @@
|
||||
group = com.muwire
|
||||
version = 0.4.4
|
||||
version = 0.5.0
|
||||
groovyVersion = 2.4.15
|
||||
slf4jVersion = 1.7.25
|
||||
spockVersion = 1.1-groovy-2.4
|
||||
grailsVersion=4.0.0
|
||||
gorm.version=7.0.2.RELEASE
|
||||
|
||||
sourceCompatibility=1.8
|
||||
targetCompatibility=1.8
|
||||
|
||||
# plugin properties
|
||||
author = zab@mail.i2p
|
||||
signer = zab@mail.i2p
|
||||
i2pVersion=0.9.41
|
||||
keystorePassword=changeit
|
||||
websiteURL=http://muwire.i2p
|
||||
updateURLsu3=http://muwire.i2p/MuWire.su3
|
||||
|
||||
pack200=true
|
||||
|
34
gradlew
vendored
34
gradlew
vendored
@@ -11,21 +11,21 @@
|
||||
PRG="$0"
|
||||
# Need this for relative symlinks.
|
||||
while [ -h "$PRG" ] ; do
|
||||
ls=`ls -ld "$PRG"`
|
||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||
ls=$(ls -ld "$PRG")
|
||||
link=$(expr "$ls" : '.*-> \(.*\)$')
|
||||
if expr "$link" : '/.*' > /dev/null; then
|
||||
PRG="$link"
|
||||
else
|
||||
PRG=`dirname "$PRG"`"/$link"
|
||||
PRG=$(dirname "$PRG")"/$link"
|
||||
fi
|
||||
done
|
||||
SAVED="`pwd`"
|
||||
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||
APP_HOME="`pwd -P`"
|
||||
SAVED="$(pwd)"
|
||||
cd "$(dirname "$PRG")/" >/dev/null
|
||||
APP_HOME="$(pwd -P)"
|
||||
cd "$SAVED" >/dev/null
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=`basename "$0"`
|
||||
APP_BASE_NAME=$(basename "$0")
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS=""
|
||||
@@ -49,7 +49,7 @@ cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "`uname`" in
|
||||
case "$(uname)" in
|
||||
CYGWIN* )
|
||||
cygwin=true
|
||||
;;
|
||||
@@ -90,7 +90,7 @@ fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||
MAX_FD_LIMIT=`ulimit -H -n`
|
||||
MAX_FD_LIMIT=$(ulimit -H -n)
|
||||
if [ $? -eq 0 ] ; then
|
||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||
MAX_FD="$MAX_FD_LIMIT"
|
||||
@@ -111,12 +111,12 @@ fi
|
||||
|
||||
# For Cygwin, switch paths to Windows format before running java
|
||||
if $cygwin ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||
APP_HOME=$(cygpath --path --mixed "$APP_HOME")
|
||||
CLASSPATH=$(cygpath --path --mixed "$CLASSPATH")
|
||||
JAVACMD=$(cygpath --unix "$JAVACMD")
|
||||
|
||||
# We build the pattern for arguments to be converted via cygpath
|
||||
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||
ROOTDIRSRAW=$(find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null)
|
||||
SEP=""
|
||||
for dir in $ROOTDIRSRAW ; do
|
||||
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||
@@ -130,13 +130,13 @@ if $cygwin ; then
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
i=0
|
||||
for arg in "$@" ; do
|
||||
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||
CHECK=$(echo "$arg"|egrep -c "$OURCYGPATTERN" -)
|
||||
CHECK2=$(echo "$arg"|egrep -c "^-") ### Determine if an option
|
||||
|
||||
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||
eval $(echo args$i)=$(cygpath --path --ignore --mixed "$arg")
|
||||
else
|
||||
eval `echo args$i`="\"$arg\""
|
||||
eval $(echo args$i)="\"$arg\""
|
||||
fi
|
||||
i=$((i+1))
|
||||
done
|
||||
|
@@ -44,9 +44,9 @@ mainClassName = 'com.muwire.gui.Launcher'
|
||||
applicationDefaultJvmArgs = ['-Djava.util.logging.config.file=logging.properties']
|
||||
|
||||
apply from: 'gradle/publishing.gradle'
|
||||
apply from: 'gradle/code-coverage.gradle'
|
||||
apply from: 'gradle/code-quality.gradle'
|
||||
apply from: 'gradle/integration-test.gradle'
|
||||
// apply from: 'gradle/code-coverage.gradle'
|
||||
// apply from: 'gradle/code-quality.gradle'
|
||||
// apply from: 'gradle/integration-test.gradle'
|
||||
// apply from: 'gradle/package.gradle'
|
||||
apply from: 'gradle/docs.gradle'
|
||||
apply plugin: 'com.github.johnrengelman.shadow'
|
||||
@@ -119,6 +119,7 @@ if (hasProperty('debugRun') && ((project.debugRun as boolean))) {
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
task jacocoRootMerge(type: org.gradle.testing.jacoco.tasks.JacocoMerge, dependsOn: [test, jacocoTestReport, jacocoIntegrationTestReport]) {
|
||||
executionData = files(jacocoTestReport.executionData, jacocoIntegrationTestReport.executionData)
|
||||
destinationFile = file("${buildDir}/jacoco/root.exec")
|
||||
@@ -138,4 +139,5 @@ task jacocoRootReport(dependsOn: jacocoRootMerge, type: JacocoReport) {
|
||||
xml.destination = file("${buildDir}/reports/jacoco/root/root.xml")
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
|
@@ -36,4 +36,29 @@ mvcGroups {
|
||||
view = 'com.muwire.gui.I2PStatusView'
|
||||
controller = 'com.muwire.gui.I2PStatusController'
|
||||
}
|
||||
'trust-list' {
|
||||
model = 'com.muwire.gui.TrustListModel'
|
||||
view = 'com.muwire.gui.TrustListView'
|
||||
controller = 'com.muwire.gui.TrustListController'
|
||||
}
|
||||
'content-panel' {
|
||||
model = 'com.muwire.gui.ContentPanelModel'
|
||||
view = 'com.muwire.gui.ContentPanelView'
|
||||
controller = 'com.muwire.gui.ContentPanelController'
|
||||
}
|
||||
'show-comment' {
|
||||
model = 'com.muwire.gui.ShowCommentModel'
|
||||
view = 'com.muwire.gui.ShowCommentView'
|
||||
controller = 'com.muwire.gui.ShowCommentController'
|
||||
}
|
||||
'add-comment' {
|
||||
model = 'com.muwire.gui.AddCommentModel'
|
||||
view = 'com.muwire.gui.AddCommentView'
|
||||
controller = 'com.muwire.gui.AddCommentController'
|
||||
}
|
||||
'browse' {
|
||||
model = 'com.muwire.gui.BrowseModel'
|
||||
view = 'com.muwire.gui.BrowseView'
|
||||
controller = 'com.muwire.gui.BrowseController'
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,45 @@
|
||||
package com.muwire.gui
|
||||
|
||||
import griffon.core.artifact.GriffonController
|
||||
import griffon.core.controller.ControllerAction
|
||||
import griffon.inject.MVCMember
|
||||
import griffon.metadata.ArtifactProviderFor
|
||||
import net.i2p.data.Base64
|
||||
|
||||
import javax.annotation.Nonnull
|
||||
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.files.UICommentEvent
|
||||
import com.muwire.core.util.DataUtil
|
||||
|
||||
@ArtifactProviderFor(GriffonController)
|
||||
class AddCommentController {
|
||||
@MVCMember @Nonnull
|
||||
AddCommentModel model
|
||||
@MVCMember @Nonnull
|
||||
AddCommentView view
|
||||
|
||||
Core core
|
||||
|
||||
@ControllerAction
|
||||
void save() {
|
||||
String comment = view.textarea.getText()
|
||||
if (comment.trim().length() == 0)
|
||||
comment = null
|
||||
else
|
||||
comment = Base64.encode(DataUtil.encodei18nString(comment))
|
||||
model.selectedFiles.each {
|
||||
def event = new UICommentEvent(sharedFile : it, oldComment : it.getComment())
|
||||
it.setComment(comment)
|
||||
core.eventBus.publish(event)
|
||||
}
|
||||
mvcGroup.parentGroup.view.refreshSharedFiles()
|
||||
cancel()
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void cancel() {
|
||||
view.dialog.setVisible(false)
|
||||
mvcGroup.destroy()
|
||||
}
|
||||
}
|
@@ -0,0 +1,95 @@
|
||||
package com.muwire.gui
|
||||
|
||||
import griffon.core.artifact.GriffonController
|
||||
import griffon.core.controller.ControllerAction
|
||||
import griffon.inject.MVCMember
|
||||
import griffon.metadata.ArtifactProviderFor
|
||||
import net.i2p.data.Base64
|
||||
|
||||
import javax.annotation.Nonnull
|
||||
|
||||
import com.muwire.core.EventBus
|
||||
import com.muwire.core.download.UIDownloadEvent
|
||||
import com.muwire.core.search.BrowseStatusEvent
|
||||
import com.muwire.core.search.UIBrowseEvent
|
||||
import com.muwire.core.search.UIResultEvent
|
||||
|
||||
@ArtifactProviderFor(GriffonController)
|
||||
class BrowseController {
|
||||
@MVCMember @Nonnull
|
||||
BrowseModel model
|
||||
@MVCMember @Nonnull
|
||||
BrowseView view
|
||||
|
||||
EventBus eventBus
|
||||
|
||||
|
||||
void register() {
|
||||
eventBus.register(BrowseStatusEvent.class, this)
|
||||
eventBus.register(UIResultEvent.class, this)
|
||||
eventBus.publish(new UIBrowseEvent(host : model.host))
|
||||
}
|
||||
|
||||
void mvcGroupDestroy() {
|
||||
eventBus.unregister(BrowseStatusEvent.class, this)
|
||||
eventBus.unregister(UIResultEvent.class, this)
|
||||
}
|
||||
|
||||
void onBrowseStatusEvent(BrowseStatusEvent e) {
|
||||
runInsideUIAsync {
|
||||
model.status = e.status
|
||||
}
|
||||
}
|
||||
|
||||
void onUIResultEvent(UIResultEvent e) {
|
||||
runInsideUIAsync {
|
||||
model.results << e
|
||||
view.resultsTable.model.fireTableDataChanged()
|
||||
}
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void dismiss() {
|
||||
view.dialog.setVisible(false)
|
||||
mvcGroup.destroy()
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void download() {
|
||||
def selectedResults = view.selectedResults()
|
||||
if (selectedResults == null || selectedResults.isEmpty())
|
||||
return
|
||||
selectedResults.removeAll {
|
||||
!mvcGroup.parentGroup.parentGroup.model.canDownload(it.infohash)
|
||||
}
|
||||
|
||||
selectedResults.each { result ->
|
||||
def file = new File(application.context.get("muwire-settings").downloadLocation, result.name)
|
||||
eventBus.publish(new UIDownloadEvent(
|
||||
result : [result],
|
||||
sources : [model.host.destination],
|
||||
target : file,
|
||||
sequential : mvcGroup.parentGroup.view.sequentialDownloadCheckbox.model.isSelected()
|
||||
))
|
||||
}
|
||||
|
||||
mvcGroup.parentGroup.parentGroup.view.showDownloadsWindow.call()
|
||||
dismiss()
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void viewComment() {
|
||||
def selectedResults = view.selectedResults()
|
||||
if (selectedResults == null || selectedResults.size() != 1)
|
||||
return
|
||||
def result = selectedResults[0]
|
||||
if (result.comment == null)
|
||||
return
|
||||
|
||||
String groupId = Base64.encode(result.infohash.getRoot())
|
||||
Map<String,Object> params = new HashMap<>()
|
||||
params['result'] = result
|
||||
|
||||
mvcGroup.createMVCGroup("show-comment", groupId, params)
|
||||
}
|
||||
}
|
@@ -0,0 +1,105 @@
|
||||
package com.muwire.gui
|
||||
|
||||
import griffon.core.artifact.GriffonController
|
||||
import griffon.core.controller.ControllerAction
|
||||
import griffon.inject.MVCMember
|
||||
import griffon.metadata.ArtifactProviderFor
|
||||
import javax.annotation.Nonnull
|
||||
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.EventBus
|
||||
import com.muwire.core.content.ContentControlEvent
|
||||
import com.muwire.core.content.Match
|
||||
import com.muwire.core.content.Matcher
|
||||
import com.muwire.core.content.RegexMatcher
|
||||
import com.muwire.core.trust.TrustEvent
|
||||
import com.muwire.core.trust.TrustLevel
|
||||
|
||||
@ArtifactProviderFor(GriffonController)
|
||||
class ContentPanelController {
|
||||
@MVCMember @Nonnull
|
||||
ContentPanelModel model
|
||||
@MVCMember @Nonnull
|
||||
ContentPanelView view
|
||||
|
||||
Core core
|
||||
|
||||
@ControllerAction
|
||||
void addRule() {
|
||||
def term = view.ruleTextField.text
|
||||
|
||||
if (model.regex)
|
||||
core.muOptions.watchedRegexes.add(term)
|
||||
else
|
||||
core.muOptions.watchedKeywords.add(term)
|
||||
saveMuWireSettings()
|
||||
|
||||
core.eventBus.publish(new ContentControlEvent(term : term, regex : model.regex, add:true))
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void deleteRule() {
|
||||
int rule = view.getSelectedRule()
|
||||
if (rule < 0)
|
||||
return
|
||||
Matcher matcher = model.rules[rule]
|
||||
String term = matcher.getTerm()
|
||||
if (matcher instanceof RegexMatcher)
|
||||
core.muOptions.watchedRegexes.remove(term)
|
||||
else
|
||||
core.muOptions.watchedKeywords.remove(term)
|
||||
saveMuWireSettings()
|
||||
|
||||
core.eventBus.publish(new ContentControlEvent(term : term, regex : (matcher instanceof RegexMatcher), add: false))
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void keyword() {
|
||||
model.regex = false
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void regex() {
|
||||
model.regex = true
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void refresh() {
|
||||
model.refresh()
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void clearHits() {
|
||||
int selectedRule = view.getSelectedRule()
|
||||
if (selectedRule < 0)
|
||||
return
|
||||
Matcher matcher = model.rules[selectedRule]
|
||||
matcher.matches.clear()
|
||||
model.refresh()
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void trust() {
|
||||
int selectedHit = view.getSelectedHit()
|
||||
if (selectedHit < 0)
|
||||
return
|
||||
Match m = model.hits[selectedHit]
|
||||
core.eventBus.publish(new TrustEvent(persona : m.persona, level : TrustLevel.TRUSTED))
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void distrust() {
|
||||
int selectedHit = view.getSelectedHit()
|
||||
if (selectedHit < 0)
|
||||
return
|
||||
Match m = model.hits[selectedHit]
|
||||
core.eventBus.publish(new TrustEvent(persona : m.persona, level : TrustLevel.DISTRUSTED))
|
||||
}
|
||||
|
||||
void saveMuWireSettings() {
|
||||
File f = new File(core.home, "MuWire.properties")
|
||||
f.withOutputStream {
|
||||
core.muOptions.write(it)
|
||||
}
|
||||
}
|
||||
}
|
@@ -22,6 +22,7 @@ class I2PStatusController {
|
||||
Core core = application.context.get("core")
|
||||
Router router = core.router
|
||||
model.networkStatus = router._context.commSystem().status.toStatusString()
|
||||
model.floodfill = router._context.netDb().floodfillEnabled()
|
||||
model.ntcpConnections = router._context.commSystem().getTransports()["NTCP"].countPeers()
|
||||
model.ssuConnections = router._context.commSystem().getTransports()["SSU"].countPeers()
|
||||
model.participatingTunnels = router._context.tunnelManager().getParticipatingCount()
|
||||
|
@@ -11,10 +11,13 @@ import net.i2p.data.Base64
|
||||
|
||||
import javax.annotation.Nonnull
|
||||
import javax.inject.Inject
|
||||
import javax.swing.JTable
|
||||
|
||||
import com.muwire.core.Constants
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.SharedFile
|
||||
import com.muwire.core.SplitPattern
|
||||
import com.muwire.core.download.Downloader
|
||||
import com.muwire.core.download.DownloadStartedEvent
|
||||
import com.muwire.core.download.UIDownloadCancelledEvent
|
||||
import com.muwire.core.download.UIDownloadEvent
|
||||
@@ -22,10 +25,13 @@ import com.muwire.core.download.UIDownloadPausedEvent
|
||||
import com.muwire.core.download.UIDownloadResumedEvent
|
||||
import com.muwire.core.files.DirectoryUnsharedEvent
|
||||
import com.muwire.core.files.FileUnsharedEvent
|
||||
import com.muwire.core.files.UIPersistFilesEvent
|
||||
import com.muwire.core.search.QueryEvent
|
||||
import com.muwire.core.search.SearchEvent
|
||||
import com.muwire.core.trust.RemoteTrustList
|
||||
import com.muwire.core.trust.TrustEvent
|
||||
import com.muwire.core.trust.TrustLevel
|
||||
import com.muwire.core.trust.TrustSubscriptionEvent
|
||||
|
||||
@ArtifactProviderFor(GriffonController)
|
||||
class MainFrameController {
|
||||
@@ -55,6 +61,7 @@ class MainFrameController {
|
||||
Map<String, Object> params = new HashMap<>()
|
||||
params["search-terms"] = search
|
||||
params["uuid"] = uuid.toString()
|
||||
params["core"] = core
|
||||
def group = mvcGroup.createMVCGroup("SearchTab", uuid.toString(), params)
|
||||
model.results[uuid.toString()] = group
|
||||
|
||||
@@ -74,13 +81,15 @@ class MainFrameController {
|
||||
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, " ")
|
||||
def replaced = search.toLowerCase().trim().replaceAll(SplitPattern.SPLIT_PATTERN, " ")
|
||||
def terms = replaced.split(" ")
|
||||
def nonEmpty = []
|
||||
terms.each { if (it.length() > 0) nonEmpty << it }
|
||||
searchEvent = new SearchEvent(searchTerms : nonEmpty, uuid : uuid, oobInfohash: true)
|
||||
searchEvent = new SearchEvent(searchTerms : nonEmpty, uuid : uuid, oobInfohash: true,
|
||||
searchComments : core.muOptions.searchComments)
|
||||
}
|
||||
core.eventBus.publish(new QueryEvent(searchEvent : searchEvent, firstHop : true,
|
||||
boolean firstHop = core.muOptions.allowUntrusted || core.muOptions.searchExtraHop
|
||||
core.eventBus.publish(new QueryEvent(searchEvent : searchEvent, firstHop : firstHop,
|
||||
replyTo: core.me.destination, receivedOn: core.me.destination,
|
||||
originator : core.me))
|
||||
}
|
||||
@@ -92,6 +101,7 @@ class MainFrameController {
|
||||
Map<String, Object> params = new HashMap<>()
|
||||
params["search-terms"] = tabTitle
|
||||
params["uuid"] = uuid.toString()
|
||||
params["core"] = core
|
||||
def group = mvcGroup.createMVCGroup("SearchTab", uuid.toString(), params)
|
||||
model.results[uuid.toString()] = group
|
||||
|
||||
@@ -102,20 +112,6 @@ class MainFrameController {
|
||||
originator : core.me))
|
||||
}
|
||||
|
||||
private def selectedResult() {
|
||||
def selected = builder.getVariable("result-tabs").getSelectedComponent()
|
||||
def group = selected.getClientProperty("mvc-group")
|
||||
def table = selected.getClientProperty("results-table")
|
||||
int row = table.getSelectedRow()
|
||||
if (row == -1)
|
||||
return
|
||||
def sortEvt = group.view.lastSortEvent
|
||||
if (sortEvt != null) {
|
||||
row = group.view.resultsTable.rowSorter.convertRowIndexToModel(row)
|
||||
}
|
||||
group.model.results[row]
|
||||
}
|
||||
|
||||
private int selectedDownload() {
|
||||
def downloadsTable = builder.getVariable("downloads-table")
|
||||
def selected = downloadsTable.getSelectedRow()
|
||||
@@ -126,39 +122,21 @@ class MainFrameController {
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void download() {
|
||||
def result = selectedResult()
|
||||
if (result == null)
|
||||
void trustPersonaFromSearch() {
|
||||
int selected = builder.getVariable("searches-table").getSelectedRow()
|
||||
if (selected < 0)
|
||||
return
|
||||
|
||||
if (!model.canDownload(result.infohash))
|
||||
return
|
||||
|
||||
def file = new File(application.context.get("muwire-settings").downloadLocation, result.name)
|
||||
|
||||
def selected = builder.getVariable("result-tabs").getSelectedComponent()
|
||||
def group = selected.getClientProperty("mvc-group")
|
||||
|
||||
def resultsBucket = group.model.hashBucket[result.infohash]
|
||||
def sources = group.model.sourcesBucket[result.infohash]
|
||||
|
||||
core.eventBus.publish(new UIDownloadEvent(result : resultsBucket, sources: sources, target : file))
|
||||
Persona p = model.searches[selected].originator
|
||||
core.eventBus.publish( new TrustEvent(persona : p, level : TrustLevel.TRUSTED) )
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void trust() {
|
||||
def result = selectedResult()
|
||||
if (result == null)
|
||||
return // TODO disable button
|
||||
core.eventBus.publish( new TrustEvent(persona : result.sender, level : TrustLevel.TRUSTED))
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void distrust() {
|
||||
def result = selectedResult()
|
||||
if (result == null)
|
||||
return // TODO disable button
|
||||
core.eventBus.publish( new TrustEvent(persona : result.sender, level : TrustLevel.DISTRUSTED))
|
||||
void distrustPersonaFromSearch() {
|
||||
int selected = builder.getVariable("searches-table").getSelectedRow()
|
||||
if (selected < 0)
|
||||
return
|
||||
Persona p = model.searches[selected].originator
|
||||
core.eventBus.publish( new TrustEvent(persona : p, level : TrustLevel.DISTRUSTED) )
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
@@ -183,50 +161,136 @@ class MainFrameController {
|
||||
core.eventBus.publish(new UIDownloadPausedEvent())
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void clear() {
|
||||
def toRemove = []
|
||||
model.downloads.each {
|
||||
if (it.downloader.getCurrentState() == Downloader.DownloadState.CANCELLED) {
|
||||
toRemove << it
|
||||
} else if (it.downloader.getCurrentState() == Downloader.DownloadState.FINISHED) {
|
||||
toRemove << it
|
||||
}
|
||||
}
|
||||
toRemove.each {
|
||||
model.downloads.remove(it)
|
||||
}
|
||||
model.clearButtonEnabled = false
|
||||
|
||||
}
|
||||
|
||||
private void markTrust(String tableName, TrustLevel level, def list) {
|
||||
int row = builder.getVariable(tableName).getSelectedRow()
|
||||
int row = view.getSelectedTrustTablesRow(tableName)
|
||||
if (row < 0)
|
||||
return
|
||||
builder.getVariable(tableName).model.fireTableDataChanged()
|
||||
core.eventBus.publish(new TrustEvent(persona : list[row], level : level))
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void markTrusted() {
|
||||
markTrust("distrusted-table", TrustLevel.TRUSTED, model.distrusted)
|
||||
model.markTrustedButtonEnabled = false
|
||||
model.markNeutralFromDistrustedButtonEnabled = false
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void markNeutralFromDistrusted() {
|
||||
markTrust("distrusted-table", TrustLevel.NEUTRAL, model.distrusted)
|
||||
model.markTrustedButtonEnabled = false
|
||||
model.markNeutralFromDistrustedButtonEnabled = false
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void markDistrusted() {
|
||||
markTrust("trusted-table", TrustLevel.DISTRUSTED, model.trusted)
|
||||
model.subscribeButtonEnabled = false
|
||||
model.markDistrustedButtonEnabled = false
|
||||
model.markNeutralFromTrustedButtonEnabled = false
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void markNeutralFromTrusted() {
|
||||
markTrust("trusted-table", TrustLevel.NEUTRAL, model.trusted)
|
||||
model.subscribeButtonEnabled = false
|
||||
model.markDistrustedButtonEnabled = false
|
||||
model.markNeutralFromTrustedButtonEnabled = false
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void subscribe() {
|
||||
int row = view.getSelectedTrustTablesRow("trusted-table")
|
||||
if (row < 0)
|
||||
return
|
||||
Persona p = model.trusted[row]
|
||||
core.muOptions.trustSubscriptions.add(p)
|
||||
saveMuWireSettings()
|
||||
core.eventBus.publish(new TrustSubscriptionEvent(persona : p, subscribe : true))
|
||||
model.subscribeButtonEnabled = false
|
||||
model.markDistrustedButtonEnabled = false
|
||||
model.markNeutralFromTrustedButtonEnabled = false
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void review() {
|
||||
RemoteTrustList list = getSelectedTrustList()
|
||||
if (list == null)
|
||||
return
|
||||
Map<String,Object> env = new HashMap<>()
|
||||
env["trustList"] = list
|
||||
env["trustService"] = core.trustService
|
||||
env["eventBus"] = core.eventBus
|
||||
mvcGroup.createMVCGroup("trust-list", env)
|
||||
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void update() {
|
||||
RemoteTrustList list = getSelectedTrustList()
|
||||
if (list == null)
|
||||
return
|
||||
core.eventBus.publish(new TrustSubscriptionEvent(persona : list.persona, subscribe : true))
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void unsubscribe() {
|
||||
RemoteTrustList list = getSelectedTrustList()
|
||||
if (list == null)
|
||||
return
|
||||
core.muOptions.trustSubscriptions.remove(list.persona)
|
||||
saveMuWireSettings()
|
||||
model.subscriptions.remove(list)
|
||||
JTable table = builder.getVariable("subscription-table")
|
||||
table.model.fireTableDataChanged()
|
||||
core.eventBus.publish(new TrustSubscriptionEvent(persona : list.persona, subscribe : false))
|
||||
}
|
||||
|
||||
private RemoteTrustList getSelectedTrustList() {
|
||||
int row = view.getSelectedTrustTablesRow("subscription-table")
|
||||
if (row < 0)
|
||||
return null
|
||||
model.subscriptions[row]
|
||||
}
|
||||
|
||||
void unshareSelectedFile() {
|
||||
SharedFile sf = view.selectedSharedFile()
|
||||
def sf = view.selectedSharedFiles()
|
||||
if (sf == null)
|
||||
return
|
||||
core.eventBus.publish(new FileUnsharedEvent(unsharedFile : sf))
|
||||
sf.each {
|
||||
core.eventBus.publish(new FileUnsharedEvent(unsharedFile : it))
|
||||
}
|
||||
core.eventBus.publish(new UIPersistFilesEvent())
|
||||
}
|
||||
|
||||
void stopWatchingDirectory() {
|
||||
String directory = mvcGroup.view.getSelectedWatchedDirectory()
|
||||
if (directory == null)
|
||||
@ControllerAction
|
||||
void addComment() {
|
||||
def selectedFiles = view.selectedSharedFiles()
|
||||
if (selectedFiles == null || selectedFiles.isEmpty())
|
||||
return
|
||||
core.muOptions.watchedDirectories.remove(directory)
|
||||
saveMuWireSettings()
|
||||
core.eventBus.publish(new DirectoryUnsharedEvent(directory : new File(directory)))
|
||||
|
||||
model.watched.remove(directory)
|
||||
builder.getVariable("watched-directories-table").model.fireTableDataChanged()
|
||||
Map<String, Object> params = new HashMap<>()
|
||||
params['selectedFiles'] = selectedFiles
|
||||
params['core'] = core
|
||||
mvcGroup.createMVCGroup("add-comment", "Add Comment", params)
|
||||
}
|
||||
|
||||
void saveMuWireSettings() {
|
||||
|
@@ -70,18 +70,27 @@ class OptionsController {
|
||||
model.updateCheckInterval = text
|
||||
settings.updateCheckInterval = Integer.valueOf(text)
|
||||
|
||||
boolean searchComments = view.searchCommentsCheckbox.model.isSelected()
|
||||
model.searchComments = searchComments
|
||||
settings.searchComments = searchComments
|
||||
|
||||
boolean autoDownloadUpdate = view.autoDownloadUpdateCheckbox.model.isSelected()
|
||||
model.autoDownloadUpdate = autoDownloadUpdate
|
||||
settings.autoDownloadUpdate = autoDownloadUpdate
|
||||
|
||||
boolean onlyTrusted = view.allowUntrustedCheckbox.model.isSelected()
|
||||
model.onlyTrusted = onlyTrusted
|
||||
settings.setAllowUntrusted(!onlyTrusted)
|
||||
|
||||
boolean shareDownloaded = view.shareDownloadedCheckbox.model.isSelected()
|
||||
model.shareDownloadedFiles = shareDownloaded
|
||||
settings.shareDownloadedFiles = shareDownloaded
|
||||
|
||||
boolean shareHidden = view.shareHiddenCheckbox.model.isSelected()
|
||||
model.shareHiddenFiles = shareHidden
|
||||
settings.shareHiddenFiles = shareHidden
|
||||
|
||||
boolean browseFiles = view.browseFilesCheckbox.model.isSelected()
|
||||
model.browseFiles = browseFiles
|
||||
settings.browseFiles = browseFiles
|
||||
|
||||
String downloadLocation = model.downloadLocation
|
||||
settings.downloadLocation = new File(downloadLocation)
|
||||
|
||||
@@ -94,6 +103,23 @@ class OptionsController {
|
||||
settings.outBw = Integer.valueOf(text)
|
||||
}
|
||||
|
||||
|
||||
boolean onlyTrusted = view.allowUntrustedCheckbox.model.isSelected()
|
||||
model.onlyTrusted = onlyTrusted
|
||||
settings.setAllowUntrusted(!onlyTrusted)
|
||||
|
||||
boolean searchExtraHop = view.searchExtraHopCheckbox.model.isSelected()
|
||||
model.searchExtraHop = searchExtraHop
|
||||
settings.searchExtraHop = searchExtraHop
|
||||
|
||||
boolean trustLists = view.allowTrustListsCheckbox.model.isSelected()
|
||||
model.trustLists = trustLists
|
||||
settings.allowTrustLists = trustLists
|
||||
|
||||
String trustListInterval = view.trustListIntervalField.text
|
||||
model.trustListInterval = trustListInterval
|
||||
settings.trustListInterval = Integer.parseInt(trustListInterval)
|
||||
|
||||
File settingsFile = new File(core.home, "MuWire.properties")
|
||||
settingsFile.withOutputStream {
|
||||
settings.write(it)
|
||||
@@ -110,9 +136,8 @@ class OptionsController {
|
||||
model.font = text
|
||||
uiSettings.font = text
|
||||
|
||||
// boolean showMonitor = view.monitorCheckbox.model.isSelected()
|
||||
// model.showMonitor = showMonitor
|
||||
// uiSettings.showMonitor = showMonitor
|
||||
uiSettings.autoFontSize = model.automaticFontSize
|
||||
uiSettings.fontSize = Integer.parseInt(view.fontSizeField.text)
|
||||
|
||||
boolean clearCancelledDownloads = view.clearCancelledDownloadsCheckbox.model.isSelected()
|
||||
model.clearCancelledDownloads = clearCancelledDownloads
|
||||
@@ -126,10 +151,6 @@ 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)
|
||||
@@ -154,4 +175,15 @@ class OptionsController {
|
||||
if (rv == JFileChooser.APPROVE_OPTION)
|
||||
model.downloadLocation = chooser.getSelectedFile().getAbsolutePath()
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void automaticFontAction() {
|
||||
model.automaticFontSize = true
|
||||
model.customFontSize = 12
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void customFontAction() {
|
||||
model.automaticFontSize = false
|
||||
}
|
||||
}
|
@@ -6,6 +6,99 @@ import griffon.inject.MVCMember
|
||||
import griffon.metadata.ArtifactProviderFor
|
||||
import javax.annotation.Nonnull
|
||||
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.download.UIDownloadEvent
|
||||
import com.muwire.core.search.UIResultEvent
|
||||
import com.muwire.core.trust.TrustEvent
|
||||
import com.muwire.core.trust.TrustLevel
|
||||
|
||||
@ArtifactProviderFor(GriffonController)
|
||||
class SearchTabController {
|
||||
|
||||
@MVCMember @Nonnull
|
||||
SearchTabModel model
|
||||
@MVCMember @Nonnull
|
||||
SearchTabView view
|
||||
|
||||
Core core
|
||||
|
||||
private def selectedResults() {
|
||||
int[] rows = view.resultsTable.getSelectedRows()
|
||||
if (rows.length == 0)
|
||||
return null
|
||||
def sortEvt = view.lastSortEvent
|
||||
if (sortEvt != null) {
|
||||
for (int i = 0; i < rows.length; i++) {
|
||||
rows[i] = view.resultsTable.rowSorter.convertRowIndexToModel(rows[i])
|
||||
}
|
||||
}
|
||||
List<UIResultEvent> results = new ArrayList<>()
|
||||
rows.each { results.add(model.results[it]) }
|
||||
results
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void download() {
|
||||
def results = selectedResults()
|
||||
if (results == null)
|
||||
return
|
||||
|
||||
results.removeAll {
|
||||
!mvcGroup.parentGroup.model.canDownload(it.infohash)
|
||||
}
|
||||
|
||||
results.each { result ->
|
||||
def file = new File(application.context.get("muwire-settings").downloadLocation, result.name)
|
||||
|
||||
def resultsBucket = model.hashBucket[result.infohash]
|
||||
def sources = model.sourcesBucket[result.infohash]
|
||||
|
||||
core.eventBus.publish(new UIDownloadEvent(result : resultsBucket, sources: sources,
|
||||
target : file, sequential : view.sequentialDownloadCheckbox.model.isSelected()))
|
||||
}
|
||||
mvcGroup.parentGroup.view.showDownloadsWindow.call()
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void trust() {
|
||||
int row = view.selectedSenderRow()
|
||||
if (row < 0)
|
||||
return
|
||||
def sender = model.senders[row]
|
||||
core.eventBus.publish( new TrustEvent(persona : sender, level : TrustLevel.TRUSTED))
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void distrust() {
|
||||
int row = view.selectedSenderRow()
|
||||
if (row < 0)
|
||||
return
|
||||
def sender = model.senders[row]
|
||||
core.eventBus.publish( new TrustEvent(persona : sender, level : TrustLevel.DISTRUSTED))
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void neutral() {
|
||||
int row = view.selectedSenderRow()
|
||||
if (row < 0)
|
||||
return
|
||||
def sender = model.senders[row]
|
||||
core.eventBus.publish( new TrustEvent(persona : sender, level : TrustLevel.NEUTRAL))
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void browse() {
|
||||
int selectedSender = view.selectedSenderRow()
|
||||
if (selectedSender < 0)
|
||||
return
|
||||
Persona sender = model.senders[selectedSender]
|
||||
|
||||
String groupId = sender.getHumanReadableName()
|
||||
Map<String,Object> params = new HashMap<>()
|
||||
params['host'] = sender
|
||||
params['eventBus'] = core.eventBus
|
||||
|
||||
mvcGroup.createMVCGroup("browse", groupId, params)
|
||||
}
|
||||
}
|
@@ -0,0 +1,19 @@
|
||||
package com.muwire.gui
|
||||
|
||||
import griffon.core.artifact.GriffonController
|
||||
import griffon.core.controller.ControllerAction
|
||||
import griffon.inject.MVCMember
|
||||
import griffon.metadata.ArtifactProviderFor
|
||||
import javax.annotation.Nonnull
|
||||
|
||||
@ArtifactProviderFor(GriffonController)
|
||||
class ShowCommentController {
|
||||
@MVCMember @Nonnull
|
||||
ShowCommentView view
|
||||
|
||||
@ControllerAction
|
||||
void dismiss() {
|
||||
view.dialog.setVisible(false)
|
||||
mvcGroup.destroy()
|
||||
}
|
||||
}
|
@@ -0,0 +1,62 @@
|
||||
package com.muwire.gui
|
||||
|
||||
import griffon.core.artifact.GriffonController
|
||||
import griffon.core.controller.ControllerAction
|
||||
import griffon.inject.MVCMember
|
||||
import griffon.metadata.ArtifactProviderFor
|
||||
import javax.annotation.Nonnull
|
||||
|
||||
import com.muwire.core.EventBus
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.trust.TrustEvent
|
||||
import com.muwire.core.trust.TrustLevel
|
||||
|
||||
@ArtifactProviderFor(GriffonController)
|
||||
class TrustListController {
|
||||
@MVCMember @Nonnull
|
||||
TrustListModel model
|
||||
@MVCMember @Nonnull
|
||||
TrustListView view
|
||||
|
||||
EventBus eventBus
|
||||
|
||||
@ControllerAction
|
||||
void trustFromTrusted() {
|
||||
int selectedRow = view.getSelectedRow("trusted-table")
|
||||
if (selectedRow < 0)
|
||||
return
|
||||
Persona p = model.trusted[selectedRow]
|
||||
eventBus.publish(new TrustEvent(persona : p, level : TrustLevel.TRUSTED))
|
||||
view.fireUpdate("trusted-table")
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void trustFromDistrusted() {
|
||||
int selectedRow = view.getSelectedRow("distrusted-table")
|
||||
if (selectedRow < 0)
|
||||
return
|
||||
Persona p = model.distrusted[selectedRow]
|
||||
eventBus.publish(new TrustEvent(persona : p, level : TrustLevel.TRUSTED))
|
||||
view.fireUpdate("distrusted-table")
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void distrustFromTrusted() {
|
||||
int selectedRow = view.getSelectedRow("trusted-table")
|
||||
if (selectedRow < 0)
|
||||
return
|
||||
Persona p = model.trusted[selectedRow]
|
||||
eventBus.publish(new TrustEvent(persona : p, level : TrustLevel.DISTRUSTED))
|
||||
view.fireUpdate("trusted-table")
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void distrustFromDistrusted() {
|
||||
int selectedRow = view.getSelectedRow("distrusted-table")
|
||||
if (selectedRow < 0)
|
||||
return
|
||||
Persona p = model.distrusted[selectedRow]
|
||||
eventBus.publish(new TrustEvent(persona : p, level : TrustLevel.DISTRUSTED))
|
||||
view.fireUpdate("distrusted-table")
|
||||
}
|
||||
}
|
@@ -10,15 +10,19 @@ import com.muwire.gui.UISettings
|
||||
|
||||
import javax.annotation.Nonnull
|
||||
import javax.inject.Inject
|
||||
import javax.swing.JLabel
|
||||
import javax.swing.JTable
|
||||
import javax.swing.LookAndFeel
|
||||
import javax.swing.UIManager
|
||||
import javax.swing.plaf.FontUIResource
|
||||
|
||||
import static griffon.util.GriffonApplicationUtils.isMacOSX
|
||||
import static groovy.swing.SwingBuilder.lookAndFeel
|
||||
|
||||
import java.awt.Font
|
||||
import java.awt.Toolkit
|
||||
import java.util.logging.Level
|
||||
import java.util.logging.LogManager
|
||||
|
||||
@Log
|
||||
class Initialize extends AbstractLifecycleHandler {
|
||||
@@ -29,6 +33,16 @@ class Initialize extends AbstractLifecycleHandler {
|
||||
|
||||
@Override
|
||||
void execute() {
|
||||
|
||||
if (System.getProperty("java.util.logging.config.file") == null) {
|
||||
log.info("No config file specified, so turning off logging")
|
||||
def names = LogManager.getLogManager().getLoggerNames()
|
||||
while(names.hasMoreElements()) {
|
||||
def name = names.nextElement()
|
||||
LogManager.getLogManager().getLogger(name).setLevel(Level.OFF)
|
||||
}
|
||||
}
|
||||
|
||||
log.info "Loading home dir"
|
||||
def portableHome = System.getProperty("portable.home")
|
||||
def home = portableHome == null ?
|
||||
@@ -43,7 +57,7 @@ class Initialize extends AbstractLifecycleHandler {
|
||||
|
||||
application.context.put("muwire-home", home.getAbsolutePath())
|
||||
|
||||
System.getProperties().setProperty("awt.useSystemAAFontSettings", "true")
|
||||
System.getProperties().setProperty("awt.useSystemAAFontSettings", "gasp")
|
||||
|
||||
def guiPropsFile = new File(home, "gui.properties")
|
||||
UISettings uiSettings
|
||||
@@ -52,25 +66,43 @@ class Initialize extends AbstractLifecycleHandler {
|
||||
guiPropsFile.withInputStream { props.load(it) }
|
||||
uiSettings = new UISettings(props)
|
||||
|
||||
def lnf
|
||||
log.info("settting user-specified lnf $uiSettings.lnf")
|
||||
try {
|
||||
lookAndFeel(uiSettings.lnf)
|
||||
lnf = 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()
|
||||
log.log(Level.WARNING,"couldn't set desired look and feel, switching to defaults", bad)
|
||||
lnf = lookAndFeel("system","gtk","metal")
|
||||
uiSettings.lnf = lnf.getID()
|
||||
}
|
||||
|
||||
if (uiSettings.font != null) {
|
||||
log.info("setting user-specified font $uiSettings.font")
|
||||
Font font = new Font(uiSettings.font, Font.PLAIN, 12)
|
||||
def defaults = UIManager.getDefaults()
|
||||
defaults.put("Button.font", font)
|
||||
defaults.put("RadioButton.font", font)
|
||||
defaults.put("Label.font", font)
|
||||
defaults.put("CheckBox.font", font)
|
||||
defaults.put("Table.font", font)
|
||||
defaults.put("TableHeader.font", font)
|
||||
// TODO: add others
|
||||
if (uiSettings.font != null || uiSettings.autoFontSize || uiSettings.fontSize > 0) {
|
||||
|
||||
FontUIResource defaultFont = lnf.getDefaults().getFont("Label.font")
|
||||
|
||||
String fontName
|
||||
if (uiSettings.font != null)
|
||||
fontName = uiSettings.font
|
||||
else
|
||||
fontName = defaultFont.getName()
|
||||
|
||||
int fontSize = defaultFont.getSize()
|
||||
if (uiSettings.autoFontSize) {
|
||||
int resolution = Toolkit.getDefaultToolkit().getScreenResolution()
|
||||
fontSize = resolution / 9;
|
||||
} else {
|
||||
fontSize = uiSettings.fontSize
|
||||
}
|
||||
|
||||
FontUIResource font = new FontUIResource(fontName, Font.PLAIN, fontSize)
|
||||
|
||||
def keys = lnf.getDefaults().keys()
|
||||
while(keys.hasMoreElements()) {
|
||||
def key = keys.nextElement()
|
||||
def value = lnf.getDefaults().get(key)
|
||||
if (value instanceof FontUIResource)
|
||||
lnf.getDefaults().put(key, font)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Properties props = new Properties()
|
||||
@@ -86,6 +118,8 @@ class Initialize extends AbstractLifecycleHandler {
|
||||
}
|
||||
} else {
|
||||
LookAndFeel chosen = lookAndFeel('system', 'gtk')
|
||||
if (chosen == null)
|
||||
chosen = lookAndFeel('metal')
|
||||
uiSettings.lnf = chosen.getID()
|
||||
log.info("ended up applying $chosen.name")
|
||||
}
|
||||
|
@@ -49,7 +49,7 @@ class Ready extends AbstractLifecycleHandler {
|
||||
log.info("creating new properties")
|
||||
props = new MuWireSettings()
|
||||
props.embeddedRouter = Boolean.parseBoolean(System.getProperties().getProperty("embeddedRouter"))
|
||||
props.updateType = System.getProperty("updateType")
|
||||
props.updateType = System.getProperty("updateType","jar")
|
||||
def nickname
|
||||
while (true) {
|
||||
nickname = JOptionPane.showInputDialog(null,
|
||||
|
12
gui/griffon-app/models/com/muwire/gui/AddCommentModel.groovy
Normal file
12
gui/griffon-app/models/com/muwire/gui/AddCommentModel.groovy
Normal file
@@ -0,0 +1,12 @@
|
||||
package com.muwire.gui
|
||||
|
||||
import com.muwire.core.SharedFile
|
||||
|
||||
import griffon.core.artifact.GriffonModel
|
||||
import griffon.transform.Observable
|
||||
import griffon.metadata.ArtifactProviderFor
|
||||
|
||||
@ArtifactProviderFor(GriffonModel)
|
||||
class AddCommentModel {
|
||||
List<SharedFile> selectedFiles
|
||||
}
|
19
gui/griffon-app/models/com/muwire/gui/BrowseModel.groovy
Normal file
19
gui/griffon-app/models/com/muwire/gui/BrowseModel.groovy
Normal file
@@ -0,0 +1,19 @@
|
||||
package com.muwire.gui
|
||||
|
||||
import com.muwire.core.Persona
|
||||
|
||||
import griffon.core.artifact.GriffonModel
|
||||
import griffon.transform.Observable
|
||||
import griffon.metadata.ArtifactProviderFor
|
||||
|
||||
import com.muwire.core.search.BrowseStatus
|
||||
|
||||
@ArtifactProviderFor(GriffonModel)
|
||||
class BrowseModel {
|
||||
Persona host
|
||||
@Observable BrowseStatus status
|
||||
@Observable boolean downloadActionEnabled
|
||||
@Observable boolean viewCommentActionEnabled
|
||||
|
||||
def results = []
|
||||
}
|
@@ -0,0 +1,59 @@
|
||||
package com.muwire.gui
|
||||
|
||||
import javax.annotation.Nonnull
|
||||
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.EventBus
|
||||
import com.muwire.core.content.ContentControlEvent
|
||||
import com.muwire.core.content.ContentManager
|
||||
|
||||
import griffon.core.artifact.GriffonModel
|
||||
import griffon.inject.MVCMember
|
||||
import griffon.transform.Observable
|
||||
import griffon.metadata.ArtifactProviderFor
|
||||
|
||||
@ArtifactProviderFor(GriffonModel)
|
||||
class ContentPanelModel {
|
||||
|
||||
@MVCMember @Nonnull
|
||||
ContentPanelView view
|
||||
|
||||
Core core
|
||||
|
||||
private ContentManager contentManager
|
||||
|
||||
def rules = []
|
||||
def hits = []
|
||||
|
||||
@Observable boolean regex
|
||||
@Observable boolean deleteButtonEnabled
|
||||
@Observable boolean trustButtonsEnabled
|
||||
|
||||
void mvcGroupInit(Map<String,String> args) {
|
||||
contentManager = application.context.get("core").contentManager
|
||||
rules.addAll(contentManager.matchers)
|
||||
core.eventBus.register(ContentControlEvent.class, this)
|
||||
}
|
||||
|
||||
void mvcGroupDestroy() {
|
||||
core.eventBus.unregister(ContentControlEvent.class, this)
|
||||
}
|
||||
|
||||
void refresh() {
|
||||
int selectedRule = view.getSelectedRule()
|
||||
rules.clear()
|
||||
rules.addAll(contentManager.matchers)
|
||||
hits.clear()
|
||||
view.rulesTable.model.fireTableDataChanged()
|
||||
view.hitsTable.model.fireTableDataChanged()
|
||||
if (selectedRule >= 0) {
|
||||
view.rulesTable.selectionModel.setSelectionInterval(selectedRule,selectedRule)
|
||||
}
|
||||
}
|
||||
|
||||
void onContentControlEvent(ContentControlEvent e) {
|
||||
runInsideUIAsync {
|
||||
refresh()
|
||||
}
|
||||
}
|
||||
}
|
@@ -15,6 +15,7 @@ class I2PStatusModel {
|
||||
@Observable int ntcpConnections
|
||||
@Observable int ssuConnections
|
||||
@Observable String networkStatus
|
||||
@Observable boolean floodfill
|
||||
@Observable int participatingTunnels
|
||||
@Observable int activePeers
|
||||
@Observable int receiveBps
|
||||
|
@@ -1,25 +1,36 @@
|
||||
package com.muwire.gui
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import java.util.Calendar
|
||||
import java.util.UUID
|
||||
|
||||
import javax.annotation.Nonnull
|
||||
import javax.inject.Inject
|
||||
import javax.swing.JOptionPane
|
||||
import javax.swing.JTable
|
||||
import javax.swing.tree.DefaultMutableTreeNode
|
||||
import javax.swing.tree.DefaultTreeModel
|
||||
import javax.swing.tree.TreeNode
|
||||
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.InfoHash
|
||||
import com.muwire.core.MuWireSettings
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.RouterDisconnectedEvent
|
||||
import com.muwire.core.SharedFile
|
||||
import com.muwire.core.connection.ConnectionAttemptStatus
|
||||
import com.muwire.core.connection.ConnectionEvent
|
||||
import com.muwire.core.connection.DisconnectionEvent
|
||||
import com.muwire.core.content.ContentControlEvent
|
||||
import com.muwire.core.download.DownloadStartedEvent
|
||||
import com.muwire.core.download.Downloader
|
||||
import com.muwire.core.files.AllFilesLoadedEvent
|
||||
import com.muwire.core.files.DirectoryUnsharedEvent
|
||||
import com.muwire.core.files.FileDownloadedEvent
|
||||
import com.muwire.core.files.FileHashedEvent
|
||||
import com.muwire.core.files.FileHashingEvent
|
||||
import com.muwire.core.files.FileLoadedEvent
|
||||
import com.muwire.core.files.FileSharedEvent
|
||||
import com.muwire.core.files.FileUnsharedEvent
|
||||
@@ -28,6 +39,8 @@ import com.muwire.core.search.UIResultBatchEvent
|
||||
import com.muwire.core.search.UIResultEvent
|
||||
import com.muwire.core.trust.TrustEvent
|
||||
import com.muwire.core.trust.TrustService
|
||||
import com.muwire.core.trust.TrustSubscriptionEvent
|
||||
import com.muwire.core.trust.TrustSubscriptionUpdatedEvent
|
||||
import com.muwire.core.update.UpdateAvailableEvent
|
||||
import com.muwire.core.update.UpdateDownloadedEvent
|
||||
import com.muwire.core.upload.UploadEvent
|
||||
@@ -51,6 +64,8 @@ class MainFrameModel {
|
||||
FactoryBuilderSupport builder
|
||||
@MVCMember @Nonnull
|
||||
MainFrameController controller
|
||||
@MVCMember @Nonnull
|
||||
MainFrameView view
|
||||
@Inject @Nonnull GriffonApplication application
|
||||
@Observable boolean coreInitialized = false
|
||||
@Observable boolean routerPresent
|
||||
@@ -58,23 +73,43 @@ class MainFrameModel {
|
||||
def results = new ConcurrentHashMap<>()
|
||||
def downloads = []
|
||||
def uploads = []
|
||||
def shared = []
|
||||
def watched = []
|
||||
boolean treeVisible = true
|
||||
def shared
|
||||
def sharedTree
|
||||
def treeRoot
|
||||
final Map<SharedFile, TreeNode> fileToNode = new HashMap<>()
|
||||
def connectionList = []
|
||||
def searches = new LinkedList()
|
||||
def trusted = []
|
||||
def distrusted = []
|
||||
def subscriptions = []
|
||||
|
||||
@Observable int connections
|
||||
@Observable String me
|
||||
@Observable boolean downloadActionEnabled
|
||||
@Observable boolean trustButtonsEnabled
|
||||
@Observable int loadedFiles
|
||||
@Observable File hashingFile
|
||||
@Observable boolean cancelButtonEnabled
|
||||
@Observable boolean retryButtonEnabled
|
||||
@Observable boolean pauseButtonEnabled
|
||||
@Observable boolean clearButtonEnabled
|
||||
@Observable String resumeButtonText
|
||||
@Observable boolean addCommentButtonEnabled
|
||||
@Observable boolean subscribeButtonEnabled
|
||||
@Observable boolean markNeutralFromTrustedButtonEnabled
|
||||
@Observable boolean markDistrustedButtonEnabled
|
||||
@Observable boolean markNeutralFromDistrustedButtonEnabled
|
||||
@Observable boolean markTrustedButtonEnabled
|
||||
@Observable boolean reviewButtonEnabled
|
||||
@Observable boolean updateButtonEnabled
|
||||
@Observable boolean unsubscribeButtonEnabled
|
||||
|
||||
private final Set<InfoHash> infoHashes = new HashSet<>()
|
||||
@Observable boolean searchesPaneButtonEnabled
|
||||
@Observable boolean downloadsPaneButtonEnabled
|
||||
@Observable boolean uploadsPaneButtonEnabled
|
||||
@Observable boolean monitorPaneButtonEnabled
|
||||
@Observable boolean trustPaneButtonEnabled
|
||||
|
||||
@Observable Downloader downloader
|
||||
|
||||
private final Set<InfoHash> downloadInfoHashes = new HashSet<>()
|
||||
|
||||
@@ -87,7 +122,12 @@ class MainFrameModel {
|
||||
void updateTablePreservingSelection(String tableName) {
|
||||
def downloadTable = builder.getVariable(tableName)
|
||||
int selectedRow = downloadTable.getSelectedRow()
|
||||
while(true) {
|
||||
try {
|
||||
downloadTable.model.fireTableDataChanged()
|
||||
break
|
||||
} catch (IllegalArgumentException iae) {} // caused by underlying model changing while table is sorted
|
||||
}
|
||||
downloadTable.selectionModel.setSelectionInterval(selectedRow,selectedRow)
|
||||
}
|
||||
|
||||
@@ -95,6 +135,10 @@ class MainFrameModel {
|
||||
|
||||
uiSettings = application.context.get("ui-settings")
|
||||
|
||||
shared = []
|
||||
treeRoot = new DefaultMutableTreeNode()
|
||||
sharedTree = new DefaultTreeModel(treeRoot)
|
||||
|
||||
Timer timer = new Timer("download-pumper", true)
|
||||
timer.schedule({
|
||||
runInsideUIAsync {
|
||||
@@ -102,18 +146,27 @@ class MainFrameModel {
|
||||
return
|
||||
|
||||
// remove cancelled or finished downloads
|
||||
if (!clearButtonEnabled || uiSettings.clearCancelledDownloads || uiSettings.clearFinishedDownloads) {
|
||||
def toRemove = []
|
||||
downloads.each {
|
||||
if (uiSettings.clearCancelledDownloads &&
|
||||
it.downloader.getCurrentState() == Downloader.DownloadState.CANCELLED)
|
||||
if (it.downloader.getCurrentState() == Downloader.DownloadState.CANCELLED) {
|
||||
if (uiSettings.clearCancelledDownloads) {
|
||||
toRemove << it
|
||||
if (uiSettings.clearFinishedDownloads &&
|
||||
it.downloader.getCurrentState() == Downloader.DownloadState.FINISHED)
|
||||
} else {
|
||||
clearButtonEnabled = true
|
||||
}
|
||||
} else if (it.downloader.getCurrentState() == Downloader.DownloadState.FINISHED) {
|
||||
if (uiSettings.clearFinishedDownloads) {
|
||||
toRemove << it
|
||||
} else {
|
||||
clearButtonEnabled = true
|
||||
}
|
||||
}
|
||||
}
|
||||
toRemove.each {
|
||||
downloads.remove(it)
|
||||
}
|
||||
}
|
||||
|
||||
builder.getVariable("uploads-table")?.model.fireTableDataChanged()
|
||||
|
||||
@@ -134,6 +187,7 @@ class MainFrameModel {
|
||||
core.eventBus.register(ConnectionEvent.class, this)
|
||||
core.eventBus.register(DisconnectionEvent.class, this)
|
||||
core.eventBus.register(FileHashedEvent.class, this)
|
||||
core.eventBus.register(FileHashingEvent.class, this)
|
||||
core.eventBus.register(FileLoadedEvent.class, this)
|
||||
core.eventBus.register(UploadEvent.class, this)
|
||||
core.eventBus.register(UploadFinishedEvent.class, this)
|
||||
@@ -145,13 +199,21 @@ class MainFrameModel {
|
||||
core.eventBus.register(RouterDisconnectedEvent.class, this)
|
||||
core.eventBus.register(AllFilesLoadedEvent.class, this)
|
||||
core.eventBus.register(UpdateDownloadedEvent.class, this)
|
||||
core.eventBus.register(TrustSubscriptionUpdatedEvent.class, this)
|
||||
|
||||
core.muOptions.watchedKeywords.each {
|
||||
core.eventBus.publish(new ContentControlEvent(term : it, regex: false, add: true))
|
||||
}
|
||||
core.muOptions.watchedRegexes.each {
|
||||
core.eventBus.publish(new ContentControlEvent(term : it, regex: true, add: true))
|
||||
}
|
||||
|
||||
timer.schedule({
|
||||
if (core.shutdown.get())
|
||||
return
|
||||
int retryInterval = core.muOptions.downloadRetryInterval
|
||||
if (retryInterval > 0) {
|
||||
retryInterval *= 60000
|
||||
retryInterval *= 1000
|
||||
long now = System.currentTimeMillis()
|
||||
if (now - lastRetryTime > retryInterval) {
|
||||
lastRetryTime = now
|
||||
@@ -167,14 +229,19 @@ class MainFrameModel {
|
||||
|
||||
}
|
||||
}
|
||||
}, 60000, 60000)
|
||||
}, 1000, 1000)
|
||||
|
||||
runInsideUIAsync {
|
||||
trusted.addAll(core.trustService.good.values())
|
||||
distrusted.addAll(core.trustService.bad.values())
|
||||
|
||||
|
||||
resumeButtonText = "Retry"
|
||||
|
||||
searchesPaneButtonEnabled = false
|
||||
downloadsPaneButtonEnabled = true
|
||||
uploadsPaneButtonEnabled = true
|
||||
monitorPaneButtonEnabled = true
|
||||
trustPaneButtonEnabled = true
|
||||
}
|
||||
})
|
||||
|
||||
@@ -182,9 +249,11 @@ class MainFrameModel {
|
||||
|
||||
void onAllFilesLoadedEvent(AllFilesLoadedEvent e) {
|
||||
runInsideUIAsync {
|
||||
watched.addAll(core.muOptions.watchedDirectories)
|
||||
builder.getVariable("watched-directories-table").model.fireTableDataChanged()
|
||||
watched.each { core.eventBus.publish(new FileSharedEvent(file : new File(it))) }
|
||||
core.muOptions.watchedDirectories.each { core.eventBus.publish(new FileSharedEvent(file : new File(it))) }
|
||||
|
||||
core.muOptions.trustSubscriptions.each {
|
||||
core.eventBus.publish(new TrustSubscriptionEvent(persona : it, subscribe : true))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -246,38 +315,63 @@ class MainFrameModel {
|
||||
}
|
||||
}
|
||||
|
||||
void onFileHashingEvent(FileHashingEvent e) {
|
||||
runInsideUIAsync {
|
||||
hashingFile = e.hashingFile
|
||||
}
|
||||
}
|
||||
|
||||
void onFileHashedEvent(FileHashedEvent e) {
|
||||
runInsideUIAsync {
|
||||
hashingFile = null
|
||||
}
|
||||
if (e.error != null)
|
||||
return // TODO do something
|
||||
if (infoHashes.contains(e.sharedFile.infoHash))
|
||||
return
|
||||
infoHashes.add(e.sharedFile.infoHash)
|
||||
runInsideUIAsync {
|
||||
shared << e.sharedFile
|
||||
loadedFiles = shared.size()
|
||||
JTable table = builder.getVariable("shared-files-table")
|
||||
table.model.fireTableDataChanged()
|
||||
insertIntoTree(e.sharedFile)
|
||||
loadedFiles = fileToNode.size()
|
||||
}
|
||||
}
|
||||
|
||||
void onFileLoadedEvent(FileLoadedEvent e) {
|
||||
if (infoHashes.contains(e.loadedFile.infoHash))
|
||||
return
|
||||
infoHashes.add(e.loadedFile.infoHash)
|
||||
runInsideUIAsync {
|
||||
shared << e.loadedFile
|
||||
loadedFiles = shared.size()
|
||||
JTable table = builder.getVariable("shared-files-table")
|
||||
table.model.fireTableDataChanged()
|
||||
insertIntoTree(e.loadedFile)
|
||||
loadedFiles = fileToNode.size()
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
loadedFiles = shared.size()
|
||||
|
||||
def dmtn = fileToNode.remove(e.unsharedFile)
|
||||
if (dmtn != null) {
|
||||
loadedFiles = fileToNode.size()
|
||||
while (true) {
|
||||
def parent = dmtn.getParent()
|
||||
parent.remove(dmtn)
|
||||
if (parent == treeRoot)
|
||||
break
|
||||
if (parent.getChildCount() == 0) {
|
||||
File file = parent.getUserObject().file
|
||||
if (core.muOptions.watchedDirectories.contains(file.toString()))
|
||||
core.eventBus.publish(new DirectoryUnsharedEvent(directory : parent.getUserObject().file))
|
||||
dmtn = parent
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
view.refreshSharedFiles()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -316,6 +410,14 @@ class MainFrameModel {
|
||||
}
|
||||
}
|
||||
|
||||
void onTrustSubscriptionUpdatedEvent(TrustSubscriptionUpdatedEvent e) {
|
||||
runInsideUIAsync {
|
||||
if (!subscriptions.contains(e.trustList))
|
||||
subscriptions << e.trustList
|
||||
updateTablePreservingSelection("subscription-table")
|
||||
}
|
||||
}
|
||||
|
||||
void onQueryEvent(QueryEvent e) {
|
||||
if (e.replyTo == core.me.destination)
|
||||
return
|
||||
@@ -337,10 +439,32 @@ class MainFrameModel {
|
||||
return
|
||||
}
|
||||
runInsideUIAsync {
|
||||
searches.addFirst(new IncomingSearch(search : search, replyTo : e.replyTo, originator : e.originator))
|
||||
JTable table = builder.getVariable("searches-table")
|
||||
|
||||
Boolean searchFound = false
|
||||
Iterator searchIter = searches.iterator()
|
||||
while ( searchIter.hasNext() ) {
|
||||
IncomingSearch searchEle = searchIter.next()
|
||||
if ( searchEle.search == search
|
||||
&& searchEle.originator == e.originator
|
||||
&& searchEle.uuid == e.searchEvent.getUuid() ) {
|
||||
searchIter.remove()
|
||||
table.model.fireTableDataChanged()
|
||||
searchFound = true
|
||||
searchEle.count++
|
||||
searchEle.timestamp = Calendar.getInstance()
|
||||
searches.addFirst(searchEle)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (!searchFound) {
|
||||
searches.addFirst(new IncomingSearch(search, e.replyTo, e.originator, e.searchEvent.getUuid()))
|
||||
}
|
||||
|
||||
while(searches.size() > 200)
|
||||
searches.removeLast()
|
||||
JTable table = builder.getVariable("searches-table")
|
||||
|
||||
table.model.fireTableDataChanged()
|
||||
}
|
||||
}
|
||||
@@ -349,6 +473,18 @@ class MainFrameModel {
|
||||
String search
|
||||
Destination replyTo
|
||||
Persona originator
|
||||
long count
|
||||
UUID uuid
|
||||
Calendar timestamp
|
||||
|
||||
IncomingSearch( String search, Destination replyTo, Persona originator, UUID uuid ) {
|
||||
this.search = search
|
||||
this.replyTo = replyTo
|
||||
this.originator = originator
|
||||
this.uuid = uuid
|
||||
this.count = 1
|
||||
this.timestamp = Calendar.getInstance()
|
||||
}
|
||||
}
|
||||
|
||||
void onUpdateAvailableEvent(UpdateAvailableEvent e) {
|
||||
@@ -374,14 +510,49 @@ class MainFrameModel {
|
||||
void onFileDownloadedEvent(FileDownloadedEvent e) {
|
||||
if (!core.muOptions.shareDownloadedFiles)
|
||||
return
|
||||
infoHashes.add(e.downloadedFile.infoHash)
|
||||
runInsideUIAsync {
|
||||
shared << e.downloadedFile
|
||||
JTable table = builder.getVariable("shared-files-table")
|
||||
table.model.fireTableDataChanged()
|
||||
insertIntoTree(e.downloadedFile)
|
||||
loadedFiles = fileToNode.size()
|
||||
}
|
||||
}
|
||||
|
||||
private void insertIntoTree(SharedFile file) {
|
||||
List<File> parents = new ArrayList<>()
|
||||
File tmp = file.file.getParentFile()
|
||||
while(tmp.getParent() != null) {
|
||||
parents << tmp
|
||||
tmp = tmp.getParentFile()
|
||||
}
|
||||
Collections.reverse(parents)
|
||||
TreeNode node = treeRoot
|
||||
for(File path : parents) {
|
||||
boolean exists = false
|
||||
def children = node.children()
|
||||
def child = null
|
||||
while(children.hasMoreElements()) {
|
||||
child = children.nextElement()
|
||||
def userObject = child.getUserObject()
|
||||
if (userObject != null && userObject.file == path) {
|
||||
exists = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!exists) {
|
||||
child = new DefaultMutableTreeNode(new InterimTreeNode(path))
|
||||
node.add(child)
|
||||
}
|
||||
node = child
|
||||
}
|
||||
|
||||
def dmtn = new DefaultMutableTreeNode(file)
|
||||
fileToNode.put(file, dmtn)
|
||||
node.add(dmtn)
|
||||
view.refreshSharedFiles()
|
||||
}
|
||||
|
||||
private static class UIConnection {
|
||||
Destination destination
|
||||
boolean incoming
|
||||
|
@@ -12,9 +12,11 @@ class OptionsModel {
|
||||
@Observable String downloadRetryInterval
|
||||
@Observable String updateCheckInterval
|
||||
@Observable boolean autoDownloadUpdate
|
||||
@Observable boolean onlyTrusted
|
||||
@Observable boolean shareDownloadedFiles
|
||||
@Observable boolean shareHiddenFiles
|
||||
@Observable String downloadLocation
|
||||
@Observable boolean searchComments
|
||||
@Observable boolean browseFiles
|
||||
|
||||
// i2p options
|
||||
@Observable String inboundLength
|
||||
@@ -28,6 +30,8 @@ class OptionsModel {
|
||||
@Observable boolean showMonitor
|
||||
@Observable String lnf
|
||||
@Observable String font
|
||||
@Observable boolean automaticFontSize
|
||||
@Observable int customFontSize
|
||||
@Observable boolean clearCancelledDownloads
|
||||
@Observable boolean clearFinishedDownloads
|
||||
@Observable boolean excludeLocalResult
|
||||
@@ -37,14 +41,23 @@ class OptionsModel {
|
||||
@Observable String inBw
|
||||
@Observable String outBw
|
||||
|
||||
// trust options
|
||||
@Observable boolean onlyTrusted
|
||||
@Observable boolean searchExtraHop
|
||||
@Observable boolean trustLists
|
||||
@Observable String trustListInterval
|
||||
|
||||
|
||||
void mvcGroupInit(Map<String, String> args) {
|
||||
MuWireSettings settings = application.context.get("muwire-settings")
|
||||
downloadRetryInterval = settings.downloadRetryInterval
|
||||
updateCheckInterval = settings.updateCheckInterval
|
||||
autoDownloadUpdate = settings.autoDownloadUpdate
|
||||
onlyTrusted = !settings.allowUntrusted()
|
||||
shareDownloadedFiles = settings.shareDownloadedFiles
|
||||
shareHiddenFiles = settings.shareHiddenFiles
|
||||
downloadLocation = settings.downloadLocation.getAbsolutePath()
|
||||
searchComments = settings.searchComments
|
||||
browseFiles = settings.browseFiles
|
||||
|
||||
Core core = application.context.get("core")
|
||||
inboundLength = core.i2pOptions["inbound.length"]
|
||||
@@ -58,6 +71,8 @@ class OptionsModel {
|
||||
showMonitor = uiSettings.showMonitor
|
||||
lnf = uiSettings.lnf
|
||||
font = uiSettings.font
|
||||
automaticFontSize = uiSettings.autoFontSize
|
||||
customFontSize = uiSettings.fontSize
|
||||
clearCancelledDownloads = uiSettings.clearCancelledDownloads
|
||||
clearFinishedDownloads = uiSettings.clearFinishedDownloads
|
||||
excludeLocalResult = uiSettings.excludeLocalResult
|
||||
@@ -67,5 +82,10 @@ class OptionsModel {
|
||||
inBw = String.valueOf(settings.inBw)
|
||||
outBw = String.valueOf(settings.outBw)
|
||||
}
|
||||
|
||||
onlyTrusted = !settings.allowUntrusted()
|
||||
searchExtraHop = settings.searchExtraHop
|
||||
trustLists = settings.allowTrustLists
|
||||
trustListInterval = String.valueOf(settings.trustListInterval)
|
||||
}
|
||||
}
|
@@ -5,6 +5,7 @@ import javax.inject.Inject
|
||||
import javax.swing.JTable
|
||||
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.search.UIResultEvent
|
||||
|
||||
import griffon.core.artifact.GriffonModel
|
||||
@@ -18,12 +19,18 @@ class SearchTabModel {
|
||||
@MVCMember @Nonnull
|
||||
FactoryBuilderSupport builder
|
||||
|
||||
@Observable boolean downloadActionEnabled
|
||||
@Observable boolean trustButtonsEnabled
|
||||
@Observable boolean browseActionEnabled
|
||||
|
||||
Core core
|
||||
UISettings uiSettings
|
||||
String uuid
|
||||
def senders = []
|
||||
def results = []
|
||||
def hashBucket = [:]
|
||||
def sourcesBucket = [:]
|
||||
def sendersBucket = new LinkedHashMap<>()
|
||||
|
||||
|
||||
void mvcGroupInit(Map<String, String> args) {
|
||||
@@ -48,6 +55,15 @@ class SearchTabModel {
|
||||
}
|
||||
bucket << e
|
||||
|
||||
def senderBucket = sendersBucket.get(e.sender)
|
||||
if (senderBucket == null) {
|
||||
senderBucket = []
|
||||
sendersBucket[e.sender] = senderBucket
|
||||
senders.clear()
|
||||
senders.addAll(sendersBucket.keySet())
|
||||
}
|
||||
senderBucket << e
|
||||
|
||||
Set sourceBucket = sourcesBucket.get(e.infohash)
|
||||
if (sourceBucket == null) {
|
||||
sourceBucket = new HashSet()
|
||||
@@ -55,8 +71,7 @@ class SearchTabModel {
|
||||
}
|
||||
sourceBucket.addAll(e.sources)
|
||||
|
||||
results << e
|
||||
JTable table = builder.getVariable("results-table")
|
||||
JTable table = builder.getVariable("senders-table")
|
||||
table.model.fireTableDataChanged()
|
||||
}
|
||||
}
|
||||
@@ -73,6 +88,14 @@ class SearchTabModel {
|
||||
hashBucket[it.infohash] = bucket
|
||||
}
|
||||
|
||||
def senderBucket = sendersBucket.get(it.sender)
|
||||
if (senderBucket == null) {
|
||||
senderBucket = []
|
||||
sendersBucket[it.sender] = senderBucket
|
||||
senders.clear()
|
||||
senders.addAll(sendersBucket.keySet())
|
||||
}
|
||||
|
||||
Set sourceBucket = sourcesBucket.get(it.infohash)
|
||||
if (sourceBucket == null) {
|
||||
sourceBucket = new HashSet()
|
||||
@@ -81,9 +104,9 @@ class SearchTabModel {
|
||||
sourceBucket.addAll(it.sources)
|
||||
|
||||
bucket << it
|
||||
results << it
|
||||
senderBucket << it
|
||||
}
|
||||
JTable table = builder.getVariable("results-table")
|
||||
JTable table = builder.getVariable("senders-table")
|
||||
table.model.fireTableDataChanged()
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,12 @@
|
||||
package com.muwire.gui
|
||||
|
||||
import com.muwire.core.search.UIResultEvent
|
||||
|
||||
import griffon.core.artifact.GriffonModel
|
||||
import griffon.transform.Observable
|
||||
import griffon.metadata.ArtifactProviderFor
|
||||
|
||||
@ArtifactProviderFor(GriffonModel)
|
||||
class ShowCommentModel {
|
||||
UIResultEvent result
|
||||
}
|
22
gui/griffon-app/models/com/muwire/gui/TrustListModel.groovy
Normal file
22
gui/griffon-app/models/com/muwire/gui/TrustListModel.groovy
Normal file
@@ -0,0 +1,22 @@
|
||||
package com.muwire.gui
|
||||
|
||||
import com.muwire.core.trust.RemoteTrustList
|
||||
import com.muwire.core.trust.TrustService
|
||||
|
||||
import griffon.core.artifact.GriffonModel
|
||||
import griffon.transform.Observable
|
||||
import griffon.metadata.ArtifactProviderFor
|
||||
|
||||
@ArtifactProviderFor(GriffonModel)
|
||||
class TrustListModel {
|
||||
RemoteTrustList trustList
|
||||
TrustService trustService
|
||||
|
||||
def trusted
|
||||
def distrusted
|
||||
|
||||
void mvcGroupInit(Map<String,String> args) {
|
||||
trusted = new ArrayList<>(trustList.good)
|
||||
distrusted = new ArrayList<>(trustList.bad)
|
||||
}
|
||||
}
|
BIN
gui/griffon-app/resources/comment.png
Normal file
BIN
gui/griffon-app/resources/comment.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 598 B |
68
gui/griffon-app/views/com/muwire/gui/AddCommentView.groovy
Normal file
68
gui/griffon-app/views/com/muwire/gui/AddCommentView.groovy
Normal file
@@ -0,0 +1,68 @@
|
||||
package com.muwire.gui
|
||||
|
||||
import griffon.core.artifact.GriffonView
|
||||
import griffon.inject.MVCMember
|
||||
import griffon.metadata.ArtifactProviderFor
|
||||
import net.i2p.data.Base64
|
||||
|
||||
import javax.swing.JDialog
|
||||
import javax.swing.SwingConstants
|
||||
|
||||
import com.muwire.core.util.DataUtil
|
||||
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.event.WindowAdapter
|
||||
import java.awt.event.WindowEvent
|
||||
|
||||
import javax.annotation.Nonnull
|
||||
|
||||
@ArtifactProviderFor(GriffonView)
|
||||
class AddCommentView {
|
||||
@MVCMember @Nonnull
|
||||
FactoryBuilderSupport builder
|
||||
@MVCMember @Nonnull
|
||||
AddCommentModel model
|
||||
|
||||
def mainFrame
|
||||
def dialog
|
||||
def p
|
||||
def textarea
|
||||
|
||||
void initUI() {
|
||||
mainFrame = application.windowManager.findWindow("main-frame")
|
||||
String title = "Add comment to multiple files"
|
||||
String comment = ""
|
||||
if (model.selectedFiles.size() == 1) {
|
||||
title = "Add comments to " + model.selectedFiles[0].getFile().getName()
|
||||
if (model.selectedFiles[0].comment != null)
|
||||
comment = DataUtil.readi18nString(Base64.decode(model.selectedFiles[0].comment))
|
||||
}
|
||||
dialog = new JDialog(mainFrame, title, true)
|
||||
|
||||
p = builder.panel {
|
||||
borderLayout()
|
||||
panel (constraints : BorderLayout.CENTER) {
|
||||
scrollPane {
|
||||
textarea = textArea(text : comment, rows : 20, columns : 100, editable : true)
|
||||
}
|
||||
}
|
||||
panel (constraints : BorderLayout.SOUTH) {
|
||||
button(text : "Save", saveAction)
|
||||
button(text : "Cancel", cancelAction)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void mvcGroupInit(Map<String,String> args) {
|
||||
dialog.getContentPane().add(p)
|
||||
dialog.pack()
|
||||
dialog.setLocationRelativeTo(mainFrame)
|
||||
dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE)
|
||||
dialog.addWindowListener( new WindowAdapter() {
|
||||
public void windowClosed(WindowEvent e) {
|
||||
mvcGroup.destroy()
|
||||
}
|
||||
})
|
||||
dialog.show()
|
||||
}
|
||||
}
|
132
gui/griffon-app/views/com/muwire/gui/BrowseView.groovy
Normal file
132
gui/griffon-app/views/com/muwire/gui/BrowseView.groovy
Normal file
@@ -0,0 +1,132 @@
|
||||
package com.muwire.gui
|
||||
|
||||
import griffon.core.artifact.GriffonView
|
||||
import griffon.inject.MVCMember
|
||||
import griffon.metadata.ArtifactProviderFor
|
||||
|
||||
import javax.swing.JDialog
|
||||
import javax.swing.JLabel
|
||||
import javax.swing.ListSelectionModel
|
||||
import javax.swing.SwingConstants
|
||||
import javax.swing.table.DefaultTableCellRenderer
|
||||
|
||||
import com.muwire.core.search.UIResultEvent
|
||||
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.event.WindowAdapter
|
||||
import java.awt.event.WindowEvent
|
||||
|
||||
import javax.annotation.Nonnull
|
||||
|
||||
@ArtifactProviderFor(GriffonView)
|
||||
class BrowseView {
|
||||
@MVCMember @Nonnull
|
||||
FactoryBuilderSupport builder
|
||||
@MVCMember @Nonnull
|
||||
BrowseModel model
|
||||
@MVCMember @Nonnull
|
||||
BrowseController controller
|
||||
|
||||
def mainFrame
|
||||
def dialog
|
||||
def p
|
||||
def resultsTable
|
||||
def lastSortEvent
|
||||
void initUI() {
|
||||
mainFrame = application.windowManager.findWindow("main-frame")
|
||||
dialog = new JDialog(mainFrame, model.host.getHumanReadableName(), true)
|
||||
dialog.setResizable(true)
|
||||
|
||||
p = builder.panel {
|
||||
borderLayout()
|
||||
panel (constraints : BorderLayout.NORTH) {
|
||||
label(text: "Status:")
|
||||
label(text: bind {model.status.toString()})
|
||||
}
|
||||
scrollPane (constraints : BorderLayout.CENTER){
|
||||
resultsTable = table(autoCreateRowSorter : true) {
|
||||
tableModel(list : model.results) {
|
||||
closureColumn(header: "Name", preferredWidth: 350, type: String, read : {row -> row.name.replace('<','_')})
|
||||
closureColumn(header: "Size", preferredWidth: 20, type: Long, read : {row -> row.size})
|
||||
closureColumn(header: "Comments", preferredWidth: 20, type: Boolean, read : {row -> row.comment != null})
|
||||
}
|
||||
}
|
||||
}
|
||||
panel (constraints : BorderLayout.SOUTH) {
|
||||
button(text : "Download", enabled : bind {model.downloadActionEnabled}, downloadAction)
|
||||
button(text : "View Comment", enabled : bind{model.viewCommentActionEnabled}, viewCommentAction)
|
||||
button(text : "Dismiss", dismissAction)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def centerRenderer = new DefaultTableCellRenderer()
|
||||
centerRenderer.setHorizontalAlignment(JLabel.CENTER)
|
||||
resultsTable.setDefaultRenderer(Integer.class,centerRenderer)
|
||||
|
||||
resultsTable.columnModel.getColumn(1).setCellRenderer(new SizeRenderer())
|
||||
|
||||
|
||||
resultsTable.rowSorter.addRowSorterListener({evt -> lastSortEvent = evt})
|
||||
resultsTable.rowSorter.setSortsOnUpdates(true)
|
||||
def selectionModel = resultsTable.getSelectionModel()
|
||||
selectionModel.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION)
|
||||
selectionModel.addListSelectionListener({
|
||||
int[] rows = resultsTable.getSelectedRows()
|
||||
if (rows.length == 0) {
|
||||
model.downloadActionEnabled = false
|
||||
model.viewCommentActionEnabled = false
|
||||
return
|
||||
}
|
||||
|
||||
if (lastSortEvent != null) {
|
||||
for (int i = 0; i < rows.length; i ++) {
|
||||
rows[i] = resultsTable.rowSorter.convertRowIndexToModel(rows[i])
|
||||
}
|
||||
}
|
||||
|
||||
boolean downloadActionEnabled = true
|
||||
if (rows.length == 1 && model.results[rows[0]].comment != null)
|
||||
model.viewCommentActionEnabled = true
|
||||
else
|
||||
model.viewCommentActionEnabled = false
|
||||
|
||||
rows.each {
|
||||
downloadActionEnabled &= mvcGroup.parentGroup.parentGroup.model.canDownload(model.results[it].infohash)
|
||||
}
|
||||
model.downloadActionEnabled = downloadActionEnabled
|
||||
})
|
||||
}
|
||||
|
||||
void mvcGroupInit(Map<String,String> args) {
|
||||
controller.register()
|
||||
|
||||
dialog.getContentPane().add(p)
|
||||
dialog.setSize(700, 400)
|
||||
dialog.setLocationRelativeTo(mainFrame)
|
||||
dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE)
|
||||
dialog.addWindowListener( new WindowAdapter() {
|
||||
public void windowClosed(WindowEvent e) {
|
||||
mvcGroup.destroy()
|
||||
}
|
||||
})
|
||||
dialog.show()
|
||||
}
|
||||
|
||||
|
||||
def selectedResults() {
|
||||
int [] rows = resultsTable.getSelectedRows()
|
||||
if (rows.length == 0)
|
||||
return null
|
||||
if (lastSortEvent != null) {
|
||||
for (int i = 0; i < rows.length; i ++) {
|
||||
rows[i] = resultsTable.rowSorter.convertRowIndexToModel(rows[i])
|
||||
}
|
||||
}
|
||||
|
||||
List<UIResultEvent> rv = new ArrayList<>()
|
||||
for (Integer i : rows)
|
||||
rv << model.results[i]
|
||||
rv
|
||||
}
|
||||
}
|
155
gui/griffon-app/views/com/muwire/gui/ContentPanelView.groovy
Normal file
155
gui/griffon-app/views/com/muwire/gui/ContentPanelView.groovy
Normal file
@@ -0,0 +1,155 @@
|
||||
package com.muwire.gui
|
||||
|
||||
import griffon.core.artifact.GriffonView
|
||||
import griffon.inject.MVCMember
|
||||
import griffon.metadata.ArtifactProviderFor
|
||||
|
||||
import javax.swing.JDialog
|
||||
import javax.swing.JLabel
|
||||
import javax.swing.ListSelectionModel
|
||||
import javax.swing.SwingConstants
|
||||
import javax.swing.table.DefaultTableCellRenderer
|
||||
|
||||
import com.muwire.core.content.Matcher
|
||||
import com.muwire.core.content.RegexMatcher
|
||||
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.event.WindowAdapter
|
||||
import java.awt.event.WindowEvent
|
||||
|
||||
import javax.annotation.Nonnull
|
||||
|
||||
@ArtifactProviderFor(GriffonView)
|
||||
class ContentPanelView {
|
||||
@MVCMember @Nonnull
|
||||
FactoryBuilderSupport builder
|
||||
@MVCMember @Nonnull
|
||||
ContentPanelModel model
|
||||
|
||||
def dialog
|
||||
def mainFrame
|
||||
def mainPanel
|
||||
|
||||
def rulesTable
|
||||
def ruleTextField
|
||||
def lastRulesSortEvent
|
||||
def hitsTable
|
||||
def lastHitsSortEvent
|
||||
|
||||
void initUI() {
|
||||
mainFrame = application.windowManager.findWindow("main-frame")
|
||||
dialog = new JDialog(mainFrame, "Content Control Panel", true)
|
||||
|
||||
mainPanel = builder.panel {
|
||||
gridLayout(rows:1, cols:2)
|
||||
panel {
|
||||
borderLayout()
|
||||
panel (constraints : BorderLayout.NORTH) {
|
||||
label(text : "Rules")
|
||||
}
|
||||
scrollPane (constraints : BorderLayout.CENTER) {
|
||||
rulesTable = table(id : "rules-table", autoCreateRowSorter : true) {
|
||||
tableModel(list : model.rules) {
|
||||
closureColumn(header: "Term", type:String, read: {row -> row.getTerm()})
|
||||
closureColumn(header: "Regex?", type:Boolean, read: {row -> row instanceof RegexMatcher})
|
||||
closureColumn(header: "Hits", type:Integer, read : {row -> row.matches.size()})
|
||||
}
|
||||
}
|
||||
}
|
||||
panel (constraints : BorderLayout.SOUTH) {
|
||||
borderLayout()
|
||||
ruleTextField = textField(constraints: BorderLayout.CENTER, action: addRuleAction)
|
||||
panel (constraints: BorderLayout.EAST) {
|
||||
buttonGroup(id : "ruleType")
|
||||
radioButton(text: "Keyword", selected : true, buttonGroup: ruleType, keywordAction)
|
||||
radioButton(text: "Regex", selected : false, buttonGroup: ruleType, regexAction)
|
||||
button(text : "Add Rule", addRuleAction)
|
||||
button(text : "Delete Rule", enabled : bind {model.deleteButtonEnabled}, deleteRuleAction)
|
||||
}
|
||||
}
|
||||
}
|
||||
panel (border : etchedBorder()){
|
||||
borderLayout()
|
||||
panel (constraints : BorderLayout.NORTH) {
|
||||
label(text : "Hits")
|
||||
}
|
||||
scrollPane(constraints : BorderLayout.CENTER) {
|
||||
hitsTable = table(id : "hits-table", autoCreateRowSorter : true) {
|
||||
tableModel(list : model.hits) {
|
||||
closureColumn(header : "Searcher", type : String, read : {row -> row.persona.getHumanReadableName()})
|
||||
closureColumn(header : "Keywords", type : String, read : {row -> row.keywords.join(" ")})
|
||||
closureColumn(header : "Date", type : String, read : {row -> String.valueOf(new Date(row.timestamp))})
|
||||
}
|
||||
}
|
||||
}
|
||||
panel (constraints : BorderLayout.SOUTH) {
|
||||
button(text : "Refresh", refreshAction)
|
||||
button(text : "Clear Hits", clearHitsAction)
|
||||
button(text : "Trust", enabled : bind {model.trustButtonsEnabled}, trustAction)
|
||||
button(text : "Distrust", enabled : bind {model.trustButtonsEnabled}, distrustAction)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int getSelectedRule() {
|
||||
int selectedRow = rulesTable.getSelectedRow()
|
||||
if (selectedRow < 0)
|
||||
return -1
|
||||
if (lastRulesSortEvent != null)
|
||||
selectedRow = rulesTable.rowSorter.convertRowIndexToModel(selectedRow)
|
||||
selectedRow
|
||||
}
|
||||
|
||||
int getSelectedHit() {
|
||||
int selectedRow = hitsTable.getSelectedRow()
|
||||
if (selectedRow < 0)
|
||||
return -1
|
||||
if (lastHitsSortEvent != null)
|
||||
selectedRow = hitsTable.rowSorter.convertRowIndexToModel(selectedRow)
|
||||
selectedRow
|
||||
}
|
||||
|
||||
void mvcGroupInit(Map<String,String> args) {
|
||||
def centerRenderer = new DefaultTableCellRenderer()
|
||||
centerRenderer.setHorizontalAlignment(JLabel.CENTER)
|
||||
rulesTable.setDefaultRenderer(Integer.class, centerRenderer)
|
||||
rulesTable.rowSorter.addRowSorterListener({evt -> lastRulesSortEvent = evt})
|
||||
rulesTable.rowSorter.setSortsOnUpdates(true)
|
||||
def selectionModel = rulesTable.getSelectionModel()
|
||||
selectionModel.setSelectionMode(ListSelectionModel.SINGLE_SELECTION)
|
||||
selectionModel.addListSelectionListener({
|
||||
int selectedRow = getSelectedRule()
|
||||
if (selectedRow < 0) {
|
||||
model.deleteButtonEnabled = false
|
||||
return
|
||||
} else {
|
||||
model.deleteButtonEnabled = true
|
||||
model.hits.clear()
|
||||
Matcher matcher = model.rules[selectedRow]
|
||||
model.hits.addAll(matcher.matches)
|
||||
hitsTable.model.fireTableDataChanged()
|
||||
}
|
||||
})
|
||||
|
||||
hitsTable.rowSorter.addRowSorterListener({evt -> lastHitsSortEvent = evt})
|
||||
hitsTable.rowSorter.setSortsOnUpdates(true)
|
||||
selectionModel = hitsTable.getSelectionModel()
|
||||
selectionModel.setSelectionMode(ListSelectionModel.SINGLE_SELECTION)
|
||||
selectionModel.addListSelectionListener({
|
||||
int selectedRow = getSelectedHit()
|
||||
model.trustButtonsEnabled = selectedRow >= 0
|
||||
})
|
||||
|
||||
dialog.getContentPane().add(mainPanel)
|
||||
dialog.pack()
|
||||
dialog.setLocationRelativeTo(mainFrame)
|
||||
dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE)
|
||||
dialog.addWindowListener(new WindowAdapter() {
|
||||
public void windowClosed(WindowEvent e) {
|
||||
mvcGroup.destroy()
|
||||
}
|
||||
})
|
||||
dialog.show()
|
||||
}
|
||||
}
|
@@ -35,20 +35,22 @@ class I2PStatusView {
|
||||
gridBagLayout()
|
||||
label(text : "Network status", constraints : gbc(gridx:0, gridy:0))
|
||||
label(text : bind {model.networkStatus}, constraints : gbc(gridx: 1, gridy:0))
|
||||
label(text : "NTCP Connections", constraints : gbc(gridx:0, gridy:1))
|
||||
label(text : bind {model.ntcpConnections}, constraints : gbc(gridx: 1, gridy:1))
|
||||
label(text : "SSU Connections", constraints : gbc(gridx:0, gridy:2))
|
||||
label(text : bind {model.ssuConnections}, constraints : gbc(gridx: 1, gridy:2))
|
||||
label(text : "Participating Tunnels", constraints : gbc(gridx:0, gridy:3))
|
||||
label(text : bind {model.participatingTunnels}, constraints : gbc(gridx: 1, gridy:3))
|
||||
label(text : "Participating Bandwidth", constraints : gbc(gridx:0, gridy:4))
|
||||
label(text : bind {model.participatingBW}, constraints : gbc(gridx: 1, gridy:4))
|
||||
label(text : "Active Peers", constraints : gbc(gridx:0, gridy:5))
|
||||
label(text : bind {model.activePeers}, constraints : gbc(gridx: 1, gridy:5))
|
||||
label(text : "Receive Bps (15 seconds)", constraints : gbc(gridx:0, gridy:6))
|
||||
label(text : bind {model.receiveBps}, constraints : gbc(gridx: 1, gridy:6))
|
||||
label(text : "Send Bps (15 seconds)", constraints : gbc(gridx:0, gridy:7))
|
||||
label(text : bind {model.sendBps}, constraints : gbc(gridx: 1, gridy:7))
|
||||
label(text: "Floodfill", constraints : gbc(gridx: 0, gridy : 1))
|
||||
label(text : bind {model.floodfill}, constraints : gbc(gridx:1, gridy:1))
|
||||
label(text : "NTCP Connections", constraints : gbc(gridx:0, gridy:2))
|
||||
label(text : bind {model.ntcpConnections}, constraints : gbc(gridx: 1, gridy:2))
|
||||
label(text : "SSU Connections", constraints : gbc(gridx:0, gridy:3))
|
||||
label(text : bind {model.ssuConnections}, constraints : gbc(gridx: 1, gridy:3))
|
||||
label(text : "Participating Tunnels", constraints : gbc(gridx:0, gridy:4))
|
||||
label(text : bind {model.participatingTunnels}, constraints : gbc(gridx: 1, gridy:4))
|
||||
label(text : "Participating Bandwidth", constraints : gbc(gridx:0, gridy:5))
|
||||
label(text : bind {model.participatingBW}, constraints : gbc(gridx: 1, gridy:5))
|
||||
label(text : "Active Peers", constraints : gbc(gridx:0, gridy:6))
|
||||
label(text : bind {model.activePeers}, constraints : gbc(gridx: 1, gridy:6))
|
||||
label(text : "Receive Bps (15 seconds)", constraints : gbc(gridx:0, gridy:7))
|
||||
label(text : bind {model.receiveBps}, constraints : gbc(gridx: 1, gridy:7))
|
||||
label(text : "Send Bps (15 seconds)", constraints : gbc(gridx:0, gridy:8))
|
||||
label(text : bind {model.sendBps}, constraints : gbc(gridx: 1, gridy:8))
|
||||
}
|
||||
|
||||
buttonsPanel = builder.panel {
|
||||
|
@@ -16,16 +16,21 @@ import javax.swing.JMenuItem
|
||||
import javax.swing.JPopupMenu
|
||||
import javax.swing.JSplitPane
|
||||
import javax.swing.JTable
|
||||
import javax.swing.JTree
|
||||
import javax.swing.ListSelectionModel
|
||||
import javax.swing.SwingConstants
|
||||
import javax.swing.TransferHandler
|
||||
import javax.swing.border.Border
|
||||
import javax.swing.table.DefaultTableCellRenderer
|
||||
import javax.swing.tree.TreeNode
|
||||
import javax.swing.tree.TreePath
|
||||
|
||||
import com.muwire.core.Constants
|
||||
import com.muwire.core.MuWireSettings
|
||||
import com.muwire.core.SharedFile
|
||||
import com.muwire.core.download.Downloader
|
||||
import com.muwire.core.files.FileSharedEvent
|
||||
|
||||
import com.muwire.core.trust.RemoteTrustList
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.CardLayout
|
||||
import java.awt.FlowLayout
|
||||
@@ -33,6 +38,7 @@ import java.awt.GridBagConstraints
|
||||
import java.awt.GridBagLayout
|
||||
import java.awt.Insets
|
||||
import java.awt.Toolkit
|
||||
import java.awt.datatransfer.DataFlavor
|
||||
import java.awt.datatransfer.StringSelection
|
||||
import java.awt.event.MouseAdapter
|
||||
import java.awt.event.MouseEvent
|
||||
@@ -53,10 +59,12 @@ class MainFrameView {
|
||||
def downloadsTable
|
||||
def lastDownloadSortEvent
|
||||
def lastSharedSortEvent
|
||||
def lastWatchedSortEvent
|
||||
def trustTablesSortEvents = [:]
|
||||
|
||||
UISettings settings
|
||||
|
||||
void initUI() {
|
||||
UISettings settings = application.context.get("ui-settings")
|
||||
settings = application.context.get("ui-settings")
|
||||
builder.with {
|
||||
application(size : [1024,768], id: 'main-frame',
|
||||
locationRelativeTo : null,
|
||||
@@ -71,6 +79,11 @@ class MainFrameView {
|
||||
menuBar {
|
||||
menu (text : "Options") {
|
||||
menuItem("Configuration", actionPerformed : {mvcGroup.createMVCGroup("Options")})
|
||||
menuItem("Content Control", actionPerformed : {
|
||||
def env = [:]
|
||||
env["core"] = model.core
|
||||
mvcGroup.createMVCGroup("content-panel", env)
|
||||
})
|
||||
}
|
||||
menu (text : "Status") {
|
||||
menuItem("MuWire", actionPerformed : {mvcGroup.createMVCGroup("mu-wire-status")})
|
||||
@@ -83,11 +96,12 @@ class MainFrameView {
|
||||
borderLayout()
|
||||
panel (constraints: BorderLayout.WEST) {
|
||||
gridLayout(rows:1, cols: 2)
|
||||
button(text: "Searches", actionPerformed : showSearchWindow)
|
||||
button(text: "Uploads", actionPerformed : showUploadsWindow)
|
||||
button(text: "Searches", enabled : bind{model.searchesPaneButtonEnabled},actionPerformed : showSearchWindow)
|
||||
button(text: "Downloads", enabled : bind{model.downloadsPaneButtonEnabled}, actionPerformed : showDownloadsWindow)
|
||||
button(text: "Uploads", enabled : bind{model.uploadsPaneButtonEnabled}, actionPerformed : showUploadsWindow)
|
||||
if (settings.showMonitor)
|
||||
button(text: "Monitor", actionPerformed : showMonitorWindow)
|
||||
button(text: "Trust", actionPerformed : showTrustWindow)
|
||||
button(text: "Monitor", enabled: bind{model.monitorPaneButtonEnabled},actionPerformed : showMonitorWindow)
|
||||
button(text: "Trust", enabled:bind{model.trustPaneButtonEnabled},actionPerformed : showTrustWindow)
|
||||
}
|
||||
panel(id: "top-panel", constraints: BorderLayout.CENTER) {
|
||||
cardLayout()
|
||||
@@ -110,31 +124,20 @@ class MainFrameView {
|
||||
panel (id: "cards-panel", constraints : BorderLayout.CENTER) {
|
||||
cardLayout()
|
||||
panel (constraints : "search window") {
|
||||
borderLayout()
|
||||
splitPane( orientation : JSplitPane.VERTICAL_SPLIT, dividerLocation : 500,
|
||||
continuousLayout : true, constraints : BorderLayout.CENTER) {
|
||||
panel (constraints : JSplitPane.TOP) {
|
||||
borderLayout()
|
||||
tabbedPane(id : "result-tabs", constraints: BorderLayout.CENTER)
|
||||
panel(constraints : BorderLayout.SOUTH) {
|
||||
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) {
|
||||
panel (constraints: "downloads window") {
|
||||
gridLayout(rows : 1, cols : 1)
|
||||
splitPane(orientation: JSplitPane.VERTICAL_SPLIT, continuousLayout : true, dividerLocation: 500 ) {
|
||||
panel {
|
||||
borderLayout()
|
||||
scrollPane (constraints : BorderLayout.CENTER) {
|
||||
downloadsTable = table(id : "downloads-table", autoCreateRowSorter : true) {
|
||||
tableModel(list: model.downloads) {
|
||||
closureColumn(header: "Name", preferredWidth: 350, type: String, read : {row -> row.downloader.file.getName()})
|
||||
closureColumn(header: "Name", preferredWidth: 300, type: String, read : {row -> row.downloader.file.getName()})
|
||||
closureColumn(header: "Status", preferredWidth: 50, type: String, read : {row -> row.downloader.getCurrentState().toString()})
|
||||
closureColumn(header: "Progress", preferredWidth: 20, type: String, read: { row ->
|
||||
int pieces = row.downloader.nPieces
|
||||
int done = row.downloader.donePieces()
|
||||
"$done/$pieces pieces".toString()
|
||||
})
|
||||
closureColumn(header: "Sources", preferredWidth : 10, type: Integer, read : {row -> row.downloader.activeWorkers()})
|
||||
closureColumn(header: "Progress", preferredWidth: 70, type: Downloader, read: { row -> row.downloader })
|
||||
closureColumn(header: "Speed", preferredWidth: 50, type:String, read :{row ->
|
||||
DataHelper.formatSize2Decimal(row.downloader.speed(), false) + "B/sec"
|
||||
})
|
||||
@@ -143,45 +146,102 @@ class MainFrameView {
|
||||
}
|
||||
panel (constraints : BorderLayout.SOUTH) {
|
||||
button(text: "Pause", enabled : bind {model.pauseButtonEnabled}, pauseAction)
|
||||
button(text: "Cancel", enabled : bind {model.cancelButtonEnabled }, cancelAction )
|
||||
button(text: bind { model.resumeButtonText }, enabled : bind {model.retryButtonEnabled}, resumeAction)
|
||||
button(text: "Cancel", enabled : bind {model.cancelButtonEnabled }, cancelAction)
|
||||
button(text: "Clear Done", enabled : bind {model.clearButtonEnabled}, clearAction)
|
||||
}
|
||||
}
|
||||
panel {
|
||||
borderLayout()
|
||||
panel(constraints : BorderLayout.NORTH) {
|
||||
label(text : "Download Details")
|
||||
}
|
||||
scrollPane(constraints : BorderLayout.CENTER) {
|
||||
panel (id : "download-details-panel") {
|
||||
cardLayout()
|
||||
panel (constraints : "select-download") {
|
||||
label(text : "Select a download to view details")
|
||||
}
|
||||
panel(constraints : "download-selected") {
|
||||
gridBagLayout()
|
||||
label(text : "Download Location:", constraints : gbc(gridx:0, gridy:0))
|
||||
label(text : bind {model.downloader?.file?.getAbsolutePath()},
|
||||
constraints: gbc(gridx:1, gridy:0, gridwidth: 2, insets : [0,0,0,20]))
|
||||
label(text : "Piece Size", constraints : gbc(gridx: 0, gridy:1))
|
||||
label(text : bind {model.downloader?.pieceSize}, constraints : gbc(gridx:1, gridy:1))
|
||||
label(text : "Known Sources:", constraints : gbc(gridx:3, gridy: 0))
|
||||
label(text : bind {model.downloader?.activeWorkers?.size()}, constraints : gbc(gridx:4, gridy:0, insets : [0,0,0,20]))
|
||||
label(text : "Active Sources:", constraints : gbc(gridx:3, gridy:1))
|
||||
label(text : bind {model.downloader?.activeWorkers()}, constraints : gbc(gridx:4, gridy:1, insets : [0,0,0,20]))
|
||||
label(text : "Total Pieces:", constraints : gbc(gridx:5, gridy: 0))
|
||||
label(text : bind {model.downloader?.nPieces}, constraints : gbc(gridx:6, gridy:0, insets : [0,0,0,20]))
|
||||
label(text : "Done Pieces:", constraints: gbc(gridx:5, gridy: 1))
|
||||
label(text : bind {model.downloader?.donePieces()}, constraints : gbc(gridx:6, gridy:1, insets : [0,0,0,20]))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
panel (constraints: "uploads window"){
|
||||
gridLayout(cols : 1, rows : 2)
|
||||
panel {
|
||||
gridLayout(cols : 2, rows : 1)
|
||||
panel {
|
||||
borderLayout()
|
||||
panel (constraints : BorderLayout.NORTH) {
|
||||
button(text : "Add directories to watch", actionPerformed : watchDirectories)
|
||||
label(text : bind {
|
||||
if (model.hashingFile == null) {
|
||||
"You can drag-and-drop files and directories here"
|
||||
} else {
|
||||
"hashing: " + model.hashingFile.getAbsolutePath() + " (" + DataHelper.formatSize2Decimal(model.hashingFile.length(), false).toString() + "B)"
|
||||
}
|
||||
scrollPane (constraints : BorderLayout.CENTER) {
|
||||
table(id : "watched-directories-table", autoCreateRowSorter: true) {
|
||||
tableModel(list : model.watched) {
|
||||
closureColumn(header: "Watched Directories", type : String, read : { it })
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
panel {
|
||||
panel (border : etchedBorder(), constraints : BorderLayout.CENTER) {
|
||||
borderLayout()
|
||||
panel (id : "shared-files-panel", constraints : BorderLayout.CENTER){
|
||||
cardLayout()
|
||||
panel (constraints : "shared files table") {
|
||||
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 : 500, type : String, read : {row -> row.getCachedPath()})
|
||||
closureColumn(header : "Size", preferredWidth : 100, type : Long, read : {row -> row.getCachedLength() })
|
||||
closureColumn(header : "Comments", preferredWidth : 100, type : Boolean, read : {it.getComment() != null})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
panel (constraints : "shared files tree") {
|
||||
borderLayout()
|
||||
scrollPane(constraints : BorderLayout.CENTER) {
|
||||
def jtree = new JTree(model.sharedTree)
|
||||
jtree.setCellRenderer(new SharedTreeRenderer())
|
||||
tree(id : "shared-files-tree", rootVisible : false, expandsSelectedPaths: true, jtree)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
panel (constraints : BorderLayout.SOUTH) {
|
||||
gridLayout(rows:1, cols:3)
|
||||
panel {
|
||||
buttonGroup(id : "sharedViewType")
|
||||
radioButton(text : "Tree", selected : true, buttonGroup : sharedViewType, actionPerformed : showSharedFilesTree)
|
||||
radioButton(text : "Table", selected : false, buttonGroup : sharedViewType, actionPerformed : showSharedFilesTable)
|
||||
}
|
||||
panel {
|
||||
button(text : "Share files", actionPerformed : shareFiles)
|
||||
button(text : "Add Comment", enabled : bind {model.addCommentButtonEnabled}, addCommentAction)
|
||||
}
|
||||
panel {
|
||||
panel {
|
||||
label("Shared:")
|
||||
label(text : bind {model.loadedFiles}, id : "shared-files-count")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
panel (border : etchedBorder()) {
|
||||
borderLayout()
|
||||
panel (constraints : BorderLayout.NORTH){
|
||||
label("Uploads")
|
||||
@@ -198,7 +258,18 @@ class MainFrameView {
|
||||
row.getDownloader()
|
||||
})
|
||||
closureColumn(header : "Remote Pieces", type : String, read : { row ->
|
||||
"${row.getDonePieces()}/${row.getTotalPieces()}".toString()
|
||||
int pieces = row.getTotalPieces()
|
||||
int done = row.getDonePieces()
|
||||
int percent = -1
|
||||
if ( pieces != 0 ) {
|
||||
percent = (done * 100) / pieces
|
||||
}
|
||||
long size = row.getTotalSize()
|
||||
String totalSize = ""
|
||||
if (size >= 0 ) {
|
||||
totalSize = " of " + DataHelper.formatSize2Decimal(size, false) + "B"
|
||||
}
|
||||
String.format("%02d", percent) + "% ${totalSize} ($done/$pieces pcs)".toString()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -245,12 +316,22 @@ class MainFrameView {
|
||||
return it.replyTo.toBase32()
|
||||
}
|
||||
})
|
||||
closureColumn(header : "Count", type : String, read : {
|
||||
it.count.toString()
|
||||
})
|
||||
closureColumn(header : "Timestamp", type : String, read : {
|
||||
String.format("%02d", it.timestamp.get(Calendar.HOUR_OF_DAY)) + ":" +
|
||||
String.format("%02d", it.timestamp.get(Calendar.MINUTE)) + ":" +
|
||||
String.format("%02d", it.timestamp.get(Calendar.SECOND))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
panel(constraints : "trust window") {
|
||||
gridLayout(rows : 2, cols : 1)
|
||||
panel {
|
||||
gridLayout(rows: 1, cols :2)
|
||||
panel (border : etchedBorder()){
|
||||
borderLayout()
|
||||
@@ -261,10 +342,11 @@ class MainFrameView {
|
||||
}
|
||||
}
|
||||
}
|
||||
panel (constraints : BorderLayout.EAST) {
|
||||
panel (constraints : BorderLayout.SOUTH) {
|
||||
gridBagLayout()
|
||||
button(text : "Mark Neutral", constraints : gbc(gridx: 0, gridy: 0), markNeutralFromTrustedAction)
|
||||
button(text : "Mark Distrusted", constraints : gbc(gridx: 0, gridy:1), markDistrustedAction)
|
||||
button(text : "Subscribe", enabled : bind {model.subscribeButtonEnabled}, constraints : gbc(gridx: 0, gridy : 0), subscribeAction)
|
||||
button(text : "Mark Neutral", enabled : bind {model.markNeutralFromTrustedButtonEnabled}, constraints : gbc(gridx: 1, gridy: 0), markNeutralFromTrustedAction)
|
||||
button(text : "Mark Distrusted", enabled : bind {model.markDistrustedButtonEnabled}, constraints : gbc(gridx: 2, gridy:0), markDistrustedAction)
|
||||
}
|
||||
}
|
||||
panel (border : etchedBorder()){
|
||||
@@ -276,10 +358,38 @@ class MainFrameView {
|
||||
}
|
||||
}
|
||||
}
|
||||
panel(constraints : BorderLayout.WEST) {
|
||||
panel(constraints : BorderLayout.SOUTH) {
|
||||
gridBagLayout()
|
||||
button(text: "Mark Neutral", constraints: gbc(gridx: 0, gridy: 0), markNeutralFromDistrustedAction)
|
||||
button(text: "Mark Trusted", constraints : gbc(gridx: 0, gridy : 1), markTrustedAction)
|
||||
button(text: "Mark Neutral", enabled : bind {model.markNeutralFromDistrustedButtonEnabled}, constraints: gbc(gridx: 0, gridy: 0), markNeutralFromDistrustedAction)
|
||||
button(text: "Mark Trusted", enabled : bind {model.markTrustedButtonEnabled}, constraints : gbc(gridx: 1, gridy : 0), markTrustedAction)
|
||||
}
|
||||
}
|
||||
}
|
||||
panel {
|
||||
borderLayout()
|
||||
panel (constraints : BorderLayout.NORTH){
|
||||
label(text : "Trust List Subscriptions")
|
||||
}
|
||||
scrollPane(constraints : BorderLayout.CENTER) {
|
||||
table(id : "subscription-table", autoCreateRowSorter : true) {
|
||||
tableModel(list : model.subscriptions) {
|
||||
closureColumn(header : "Name", preferredWidth: 200, type: String, read : {it.persona.getHumanReadableName()})
|
||||
closureColumn(header : "Trusted", preferredWidth : 20, type: Integer, read : {it.good.size()})
|
||||
closureColumn(header : "Distrusted", preferredWidth: 20, type: Integer, read : {it.bad.size()})
|
||||
closureColumn(header : "Status", preferredWidth: 30, type: String, read : {it.status.toString()})
|
||||
closureColumn(header : "Last Updated", preferredWidth: 200, type : String, read : {
|
||||
if (it.timestamp == 0)
|
||||
return "Never"
|
||||
else
|
||||
return String.valueOf(new Date(it.timestamp))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
panel(constraints : BorderLayout.SOUTH) {
|
||||
button(text : "Review", enabled : bind {model.reviewButtonEnabled}, reviewAction)
|
||||
button(text : "Update", enabled : bind {model.updateButtonEnabled}, updateAction)
|
||||
button(text : "Unsubscribe", enabled : bind {model.unsubscribeButtonEnabled}, unsubscribeAction)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -298,20 +408,41 @@ class MainFrameView {
|
||||
}
|
||||
|
||||
void mvcGroupInit(Map<String, String> args) {
|
||||
|
||||
def mainFrame = builder.getVariable("main-frame")
|
||||
mainFrame.setTransferHandler(new TransferHandler() {
|
||||
public boolean canImport(TransferHandler.TransferSupport support) {
|
||||
return support.isDataFlavorSupported(DataFlavor.javaFileListFlavor)
|
||||
}
|
||||
public boolean importData(TransferHandler.TransferSupport support) {
|
||||
def files = support.getTransferable().getTransferData(DataFlavor.javaFileListFlavor)
|
||||
files.each {
|
||||
model.core.eventBus.publish(new FileSharedEvent(file : it))
|
||||
}
|
||||
showUploadsWindow.call()
|
||||
true
|
||||
}
|
||||
})
|
||||
|
||||
def downloadsTable = builder.getVariable("downloads-table")
|
||||
def selectionModel = downloadsTable.getSelectionModel()
|
||||
selectionModel.setSelectionMode(ListSelectionModel.SINGLE_SELECTION)
|
||||
selectionModel.addListSelectionListener({
|
||||
def downloadDetailsPanel = builder.getVariable("download-details-panel")
|
||||
int selectedRow = selectedDownloaderRow()
|
||||
if (selectedRow < 0) {
|
||||
model.cancelButtonEnabled = false
|
||||
model.retryButtonEnabled = false
|
||||
model.pauseButtonEnabled = false
|
||||
model.downloader = null
|
||||
downloadDetailsPanel.getLayout().show(downloadDetailsPanel,"select-download")
|
||||
return
|
||||
}
|
||||
def downloader = model.downloads[selectedRow]?.downloader
|
||||
if (downloader == null)
|
||||
return
|
||||
model.downloader = downloader
|
||||
downloadDetailsPanel.getLayout().show(downloadDetailsPanel,"download-selected")
|
||||
switch(downloader.getCurrentState()) {
|
||||
case Downloader.DownloadState.CONNECTING :
|
||||
case Downloader.DownloadState.DOWNLOADING :
|
||||
@@ -342,9 +473,11 @@ class MainFrameView {
|
||||
def centerRenderer = new DefaultTableCellRenderer()
|
||||
centerRenderer.setHorizontalAlignment(JLabel.CENTER)
|
||||
downloadsTable.setDefaultRenderer(Integer.class, centerRenderer)
|
||||
downloadsTable.setDefaultRenderer(Downloader.class, new DownloadProgressRenderer())
|
||||
|
||||
downloadsTable.rowSorter.addRowSorterListener({evt -> lastDownloadSortEvent = evt})
|
||||
downloadsTable.rowSorter.setSortsOnUpdates(true)
|
||||
downloadsTable.rowSorter.setComparator(2, new DownloaderComparator())
|
||||
|
||||
downloadsTable.addMouseListener(new MouseAdapter() {
|
||||
@Override
|
||||
@@ -359,13 +492,7 @@ class MainFrameView {
|
||||
}
|
||||
})
|
||||
|
||||
// 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)
|
||||
|
||||
// shared files menu
|
||||
JPopupMenu sharedFilesMenu = new JPopupMenu()
|
||||
JMenuItem copyHashToClipboard = new JMenuItem("Copy hash to clipboard")
|
||||
copyHashToClipboard.addActionListener({mvcGroup.view.copyHashToClipboard()})
|
||||
@@ -373,7 +500,11 @@ class MainFrameView {
|
||||
JMenuItem unshareSelectedFiles = new JMenuItem("Unshare selected files")
|
||||
unshareSelectedFiles.addActionListener({mvcGroup.controller.unshareSelectedFile()})
|
||||
sharedFilesMenu.add(unshareSelectedFiles)
|
||||
sharedFilesTable.addMouseListener(new MouseAdapter() {
|
||||
JMenuItem commentSelectedFiles = new JMenuItem("Comment selected files")
|
||||
commentSelectedFiles.addActionListener({mvcGroup.controller.addComment()})
|
||||
sharedFilesMenu.add(commentSelectedFiles)
|
||||
|
||||
def sharedFilesMouseListener = new MouseAdapter() {
|
||||
@Override
|
||||
public void mouseReleased(MouseEvent e) {
|
||||
if (e.isPopupTrigger())
|
||||
@@ -384,14 +515,48 @@ class MainFrameView {
|
||||
if (e.isPopupTrigger())
|
||||
showPopupMenu(sharedFilesMenu, e)
|
||||
}
|
||||
}
|
||||
|
||||
// shared files table and tree
|
||||
def sharedFilesTable = builder.getVariable("shared-files-table")
|
||||
sharedFilesTable.columnModel.getColumn(1).setCellRenderer(new SizeRenderer())
|
||||
|
||||
sharedFilesTable.rowSorter.addRowSorterListener({evt -> lastSharedSortEvent = evt})
|
||||
sharedFilesTable.rowSorter.setSortsOnUpdates(true)
|
||||
|
||||
sharedFilesTable.addMouseListener(sharedFilesMouseListener)
|
||||
|
||||
selectionModel = sharedFilesTable.getSelectionModel()
|
||||
selectionModel.addListSelectionListener({
|
||||
def selectedFiles = selectedSharedFiles()
|
||||
if (selectedFiles == null || selectedFiles.isEmpty())
|
||||
return
|
||||
model.addCommentButtonEnabled = true
|
||||
})
|
||||
def sharedFilesTree = builder.getVariable("shared-files-tree")
|
||||
sharedFilesTree.addMouseListener(sharedFilesMouseListener)
|
||||
|
||||
sharedFilesTree.addTreeSelectionListener({
|
||||
def selectedNode = sharedFilesTree.getLastSelectedPathComponent()
|
||||
model.addCommentButtonEnabled = selectedNode != null
|
||||
|
||||
})
|
||||
|
||||
// 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)})
|
||||
JMenuItem trustSearcher = new JMenuItem("Trust searcher")
|
||||
trustSearcher.addActionListener({mvcGroup.controller.trustPersonaFromSearch()})
|
||||
JMenuItem distrustSearcher = new JMenuItem("Distrust searcher")
|
||||
distrustSearcher.addActionListener({mvcGroup.controller.distrustPersonaFromSearch()})
|
||||
|
||||
searchTableMenu.add(copySearchToClipboard)
|
||||
searchTableMenu.add(trustSearcher)
|
||||
searchTableMenu.add(distrustSearcher)
|
||||
|
||||
searchesTable.addMouseListener(new MouseAdapter() {
|
||||
@Override
|
||||
public void mouseReleased(MouseEvent e) {
|
||||
@@ -405,49 +570,138 @@ class MainFrameView {
|
||||
}
|
||||
})
|
||||
|
||||
// watched directories table
|
||||
def watchedTable = builder.getVariable("watched-directories-table")
|
||||
watchedTable.rowSorter.addRowSorterListener({evt -> lastWatchedSortEvent = evt})
|
||||
watchedTable.rowSorter.setSortsOnUpdates(true)
|
||||
JPopupMenu watchedMenu = new JPopupMenu()
|
||||
JMenuItem stopWatching = new JMenuItem("Stop sharing")
|
||||
stopWatching.addActionListener({mvcGroup.controller.stopWatchingDirectory()})
|
||||
watchedMenu.add(stopWatching)
|
||||
watchedTable.addMouseListener(new MouseAdapter() {
|
||||
@Override
|
||||
public void mouseReleased(MouseEvent e) {
|
||||
if (e.isPopupTrigger())
|
||||
showPopupMenu(watchedMenu, e)
|
||||
// subscription table
|
||||
def subscriptionTable = builder.getVariable("subscription-table")
|
||||
subscriptionTable.setDefaultRenderer(Integer.class, centerRenderer)
|
||||
subscriptionTable.rowSorter.addRowSorterListener({evt -> trustTablesSortEvents["subscription-table"] = evt})
|
||||
subscriptionTable.rowSorter.setSortsOnUpdates(true)
|
||||
selectionModel = subscriptionTable.getSelectionModel()
|
||||
selectionModel.setSelectionMode(ListSelectionModel.SINGLE_SELECTION)
|
||||
selectionModel.addListSelectionListener({
|
||||
int selectedRow = getSelectedTrustTablesRow("subscription-table")
|
||||
if (selectedRow < 0) {
|
||||
model.reviewButtonEnabled = false
|
||||
model.updateButtonEnabled = false
|
||||
model.unsubscribeButtonEnabled = false
|
||||
return
|
||||
}
|
||||
@Override
|
||||
public void mousePressed(MouseEvent e) {
|
||||
if (e.isPopupTrigger())
|
||||
showPopupMenu(watchedMenu, e)
|
||||
def trustList = model.subscriptions[selectedRow]
|
||||
if (trustList == null)
|
||||
return
|
||||
switch(trustList.status) {
|
||||
case RemoteTrustList.Status.NEW:
|
||||
case RemoteTrustList.Status.UPDATING:
|
||||
model.reviewButtonEnabled = false
|
||||
model.updateButtonEnabled = false
|
||||
model.unsubscribeButtonEnabled = false
|
||||
break
|
||||
case RemoteTrustList.Status.UPDATED:
|
||||
model.reviewButtonEnabled = true
|
||||
model.updateButtonEnabled = true
|
||||
model.unsubscribeButtonEnabled = true
|
||||
break
|
||||
case RemoteTrustList.Status.UPDATE_FAILED:
|
||||
model.reviewButtonEnabled = false
|
||||
model.updateButtonEnabled = true
|
||||
model.unsubscribeButtonEnabled = true
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
// trusted table
|
||||
def trustedTable = builder.getVariable("trusted-table")
|
||||
trustedTable.rowSorter.addRowSorterListener({evt -> trustTablesSortEvents["trusted-table"] = evt})
|
||||
trustedTable.rowSorter.setSortsOnUpdates(true)
|
||||
selectionModel = trustedTable.getSelectionModel()
|
||||
selectionModel.setSelectionMode(ListSelectionModel.SINGLE_SELECTION)
|
||||
selectionModel.addListSelectionListener({
|
||||
int selectedRow = getSelectedTrustTablesRow("trusted-table")
|
||||
if (selectedRow < 0) {
|
||||
model.subscribeButtonEnabled = false
|
||||
model.markDistrustedButtonEnabled = false
|
||||
model.markNeutralFromTrustedButtonEnabled = false
|
||||
} else {
|
||||
model.subscribeButtonEnabled = true
|
||||
model.markDistrustedButtonEnabled = true
|
||||
model.markNeutralFromTrustedButtonEnabled = true
|
||||
}
|
||||
})
|
||||
|
||||
// distrusted table
|
||||
def distrustedTable = builder.getVariable("distrusted-table")
|
||||
distrustedTable.rowSorter.addRowSorterListener({evt -> trustTablesSortEvents["distrusted-table"] = evt})
|
||||
distrustedTable.rowSorter.setSortsOnUpdates(true)
|
||||
selectionModel = distrustedTable.getSelectionModel()
|
||||
selectionModel.setSelectionMode(ListSelectionModel.SINGLE_SELECTION)
|
||||
selectionModel.addListSelectionListener({
|
||||
int selectedRow = getSelectedTrustTablesRow("distrusted-table")
|
||||
if (selectedRow < 0) {
|
||||
model.markTrustedButtonEnabled = false
|
||||
model.markNeutralFromDistrustedButtonEnabled = false
|
||||
} else {
|
||||
model.markTrustedButtonEnabled = true
|
||||
model.markNeutralFromDistrustedButtonEnabled = true
|
||||
}
|
||||
})
|
||||
|
||||
// show tree by default
|
||||
showSharedFilesTree.call()
|
||||
}
|
||||
|
||||
private static void showPopupMenu(JPopupMenu menu, MouseEvent event) {
|
||||
menu.show(event.getComponent(), event.getX(), event.getY())
|
||||
}
|
||||
|
||||
def selectedSharedFile() {
|
||||
def selectedSharedFiles() {
|
||||
if (!model.treeVisible) {
|
||||
def sharedFilesTable = builder.getVariable("shared-files-table")
|
||||
int selected = sharedFilesTable.getSelectedRow()
|
||||
if (selected < 0)
|
||||
int[] selected = sharedFilesTable.getSelectedRows()
|
||||
if (selected.length == 0)
|
||||
return null
|
||||
if (lastSharedSortEvent != null)
|
||||
selected = sharedFilesTable.rowSorter.convertRowIndexToModel(selected)
|
||||
model.shared[selected]
|
||||
List<SharedFile> rv = new ArrayList<>()
|
||||
if (lastSharedSortEvent != null) {
|
||||
for (int i = 0; i < selected.length; i ++) {
|
||||
selected[i] = sharedFilesTable.rowSorter.convertRowIndexToModel(selected[i])
|
||||
}
|
||||
}
|
||||
selected.each {
|
||||
rv.add(model.shared[it])
|
||||
}
|
||||
return rv
|
||||
} else {
|
||||
def sharedFilesTree = builder.getVariable("shared-files-tree")
|
||||
List<SharedFile> rv = new ArrayList<>()
|
||||
for (TreePath path : sharedFilesTree.getSelectionPaths()) {
|
||||
getLeafs(path.getLastPathComponent(), rv)
|
||||
}
|
||||
return rv
|
||||
}
|
||||
}
|
||||
|
||||
private static void getLeafs(TreeNode node, List<SharedFile> dest) {
|
||||
if (node.isLeaf()) {
|
||||
dest.add(node.getUserObject())
|
||||
return
|
||||
}
|
||||
def children = node.children()
|
||||
while(children.hasMoreElements()) {
|
||||
getLeafs(children.nextElement(), dest)
|
||||
}
|
||||
}
|
||||
|
||||
def copyHashToClipboard() {
|
||||
def selected = selectedSharedFile()
|
||||
if (selected == null)
|
||||
def selectedFiles = selectedSharedFiles()
|
||||
if (selectedFiles == null)
|
||||
return
|
||||
String roots = ""
|
||||
for (Iterator<SharedFile> iterator = selectedFiles.iterator(); iterator.hasNext(); ) {
|
||||
SharedFile selected = iterator.next()
|
||||
String root = Base64.encode(selected.infoHash.getRoot())
|
||||
StringSelection selection = new StringSelection(root)
|
||||
roots += root
|
||||
if (iterator.hasNext())
|
||||
roots += "\n"
|
||||
}
|
||||
StringSelection selection = new StringSelection(roots)
|
||||
def clipboard = Toolkit.getDefaultToolkit().getSystemClipboard()
|
||||
clipboard.setContents(selection, null)
|
||||
}
|
||||
@@ -537,57 +791,95 @@ class MainFrameView {
|
||||
def showSearchWindow = {
|
||||
def cardsPanel = builder.getVariable("cards-panel")
|
||||
cardsPanel.getLayout().show(cardsPanel, "search window")
|
||||
model.searchesPaneButtonEnabled = false
|
||||
model.downloadsPaneButtonEnabled = true
|
||||
model.uploadsPaneButtonEnabled = true
|
||||
model.monitorPaneButtonEnabled = true
|
||||
model.trustPaneButtonEnabled = true
|
||||
}
|
||||
|
||||
def showDownloadsWindow = {
|
||||
def cardsPanel = builder.getVariable("cards-panel")
|
||||
cardsPanel.getLayout().show(cardsPanel, "downloads window")
|
||||
model.searchesPaneButtonEnabled = true
|
||||
model.downloadsPaneButtonEnabled = false
|
||||
model.uploadsPaneButtonEnabled = true
|
||||
model.monitorPaneButtonEnabled = true
|
||||
model.trustPaneButtonEnabled = true
|
||||
}
|
||||
|
||||
def showUploadsWindow = {
|
||||
def cardsPanel = builder.getVariable("cards-panel")
|
||||
cardsPanel.getLayout().show(cardsPanel, "uploads window")
|
||||
model.searchesPaneButtonEnabled = true
|
||||
model.downloadsPaneButtonEnabled = true
|
||||
model.uploadsPaneButtonEnabled = false
|
||||
model.monitorPaneButtonEnabled = true
|
||||
model.trustPaneButtonEnabled = true
|
||||
}
|
||||
|
||||
def showMonitorWindow = {
|
||||
def cardsPanel = builder.getVariable("cards-panel")
|
||||
cardsPanel.getLayout().show(cardsPanel,"monitor window")
|
||||
model.searchesPaneButtonEnabled = true
|
||||
model.downloadsPaneButtonEnabled = true
|
||||
model.uploadsPaneButtonEnabled = true
|
||||
model.monitorPaneButtonEnabled = false
|
||||
model.trustPaneButtonEnabled = true
|
||||
}
|
||||
|
||||
def showTrustWindow = {
|
||||
def cardsPanel = builder.getVariable("cards-panel")
|
||||
cardsPanel.getLayout().show(cardsPanel,"trust window")
|
||||
model.searchesPaneButtonEnabled = true
|
||||
model.downloadsPaneButtonEnabled = true
|
||||
model.uploadsPaneButtonEnabled = true
|
||||
model.monitorPaneButtonEnabled = true
|
||||
model.trustPaneButtonEnabled = false
|
||||
}
|
||||
|
||||
def showSharedFilesTable = {
|
||||
model.treeVisible = false
|
||||
def cardsPanel = builder.getVariable("shared-files-panel")
|
||||
cardsPanel.getLayout().show(cardsPanel, "shared files table")
|
||||
}
|
||||
|
||||
def showSharedFilesTree = {
|
||||
model.treeVisible = true
|
||||
def cardsPanel = builder.getVariable("shared-files-panel")
|
||||
cardsPanel.getLayout().show(cardsPanel, "shared files tree")
|
||||
}
|
||||
|
||||
def shareFiles = {
|
||||
def chooser = new JFileChooser()
|
||||
chooser.setFileHidingEnabled(false)
|
||||
chooser.setFileHidingEnabled(!model.core.muOptions.shareHiddenFiles)
|
||||
chooser.setDialogTitle("Select file to share")
|
||||
chooser.setFileSelectionMode(JFileChooser.FILES_ONLY)
|
||||
chooser.setMultiSelectionEnabled(true)
|
||||
int rv = chooser.showOpenDialog(null)
|
||||
if (rv == JFileChooser.APPROVE_OPTION) {
|
||||
model.core.eventBus.publish(new FileSharedEvent(file : chooser.getSelectedFile()))
|
||||
chooser.getSelectedFiles().each {
|
||||
File canonical = it.getCanonicalFile()
|
||||
model.core.eventBus.publish(new FileSharedEvent(file : canonical))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def watchDirectories = {
|
||||
def chooser = new JFileChooser()
|
||||
chooser.setFileHidingEnabled(false)
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
String getSelectedWatchedDirectory() {
|
||||
def watchedTable = builder.getVariable("watched-directories-table")
|
||||
int selectedRow = watchedTable.getSelectedRow()
|
||||
int getSelectedTrustTablesRow(String tableName) {
|
||||
def table = builder.getVariable(tableName)
|
||||
int selectedRow = table.getSelectedRow()
|
||||
if (selectedRow < 0)
|
||||
return null
|
||||
if (lastWatchedSortEvent != null)
|
||||
selectedRow = watchedTable.rowSorter.convertRowIndexToModel(selectedRow)
|
||||
model.watched[selectedRow]
|
||||
return -1
|
||||
if (trustTablesSortEvents.get(tableName) != null)
|
||||
selectedRow = table.rowSorter.convertRowIndexToModel(selectedRow)
|
||||
selectedRow
|
||||
}
|
||||
|
||||
public void refreshSharedFiles() {
|
||||
def tree = builder.getVariable("shared-files-tree")
|
||||
TreePath[] selectedPaths = tree.getSelectionPaths()
|
||||
model.sharedTree.nodeStructureChanged(model.treeRoot)
|
||||
tree.setSelectionPaths(selectedPaths)
|
||||
builder.getVariable("shared-files-table").model.fireTableDataChanged()
|
||||
}
|
||||
}
|
@@ -12,6 +12,7 @@ import javax.swing.SwingConstants
|
||||
import com.muwire.core.Core
|
||||
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.GridBagConstraints
|
||||
import java.awt.event.WindowAdapter
|
||||
import java.awt.event.WindowEvent
|
||||
|
||||
@@ -29,12 +30,15 @@ class OptionsView {
|
||||
def i
|
||||
def u
|
||||
def bandwidth
|
||||
def trust
|
||||
|
||||
def retryField
|
||||
def updateField
|
||||
def autoDownloadUpdateCheckbox
|
||||
def allowUntrustedCheckbox
|
||||
def shareDownloadedCheckbox
|
||||
def shareHiddenCheckbox
|
||||
def searchCommentsCheckbox
|
||||
def browseFilesCheckbox
|
||||
|
||||
def inboundLengthField
|
||||
def inboundQuantityField
|
||||
@@ -46,15 +50,20 @@ class OptionsView {
|
||||
def lnfField
|
||||
def monitorCheckbox
|
||||
def fontField
|
||||
def fontSizeField
|
||||
def clearCancelledDownloadsCheckbox
|
||||
def clearFinishedDownloadsCheckbox
|
||||
def excludeLocalResultCheckbox
|
||||
def showSearchHashesCheckbox
|
||||
|
||||
|
||||
def inBwField
|
||||
def outBwField
|
||||
|
||||
def allowUntrustedCheckbox
|
||||
def searchExtraHopCheckbox
|
||||
def allowTrustListsCheckbox
|
||||
def trustListIntervalField
|
||||
|
||||
def buttonsPanel
|
||||
|
||||
def mainFrame
|
||||
@@ -65,26 +74,32 @@ class OptionsView {
|
||||
d.setResizable(false)
|
||||
p = builder.panel {
|
||||
gridBagLayout()
|
||||
label(text : "Retry failed downloads every", constraints : gbc(gridx: 0, gridy: 0))
|
||||
retryField = textField(text : bind { model.downloadRetryInterval }, columns : 2, constraints : gbc(gridx: 1, gridy: 0))
|
||||
label(text : "minutes", constraints : gbc(gridx : 2, gridy: 0))
|
||||
label(text : "Search in comments", constraints:gbc(gridx: 0, gridy:0))
|
||||
searchCommentsCheckbox = checkBox(selected : bind {model.searchComments}, constraints : gbc(gridx:1, gridy:0))
|
||||
|
||||
label(text : "Check for updates every", constraints : gbc(gridx : 0, gridy: 1))
|
||||
updateField = textField(text : bind {model.updateCheckInterval }, columns : 2, constraints : gbc(gridx : 1, gridy: 1))
|
||||
label(text : "hours", constraints : gbc(gridx: 2, gridy : 1))
|
||||
label(text : "Retry failed downloads every", constraints : gbc(gridx: 0, gridy: 1))
|
||||
retryField = textField(text : bind { model.downloadRetryInterval }, columns : 2, constraints : gbc(gridx: 1, gridy: 1))
|
||||
label(text : "seconds", constraints : gbc(gridx : 2, gridy: 1))
|
||||
|
||||
label(text : "Download updates automatically", constraints: gbc(gridx :0, gridy : 2))
|
||||
autoDownloadUpdateCheckbox = checkBox(selected : bind {model.autoDownloadUpdate}, constraints : gbc(gridx:1, gridy : 2))
|
||||
label(text : "Check for updates every", constraints : gbc(gridx : 0, gridy: 2))
|
||||
updateField = textField(text : bind {model.updateCheckInterval }, columns : 2, constraints : gbc(gridx : 1, gridy: 2))
|
||||
label(text : "hours", constraints : gbc(gridx: 2, gridy : 2))
|
||||
|
||||
label(text : "Allow only trusted connections", constraints : gbc(gridx: 0, gridy : 3))
|
||||
allowUntrustedCheckbox = checkBox(selected : bind {model.onlyTrusted}, constraints : gbc(gridx: 1, gridy : 3))
|
||||
label(text : "Download updates automatically", constraints: gbc(gridx :0, gridy : 3))
|
||||
autoDownloadUpdateCheckbox = checkBox(selected : bind {model.autoDownloadUpdate}, constraints : gbc(gridx:1, gridy : 3))
|
||||
|
||||
label(text : "Share downloaded files", constraints : gbc(gridx : 0, gridy:4))
|
||||
shareDownloadedCheckbox = checkBox(selected : bind {model.shareDownloadedFiles}, constraints : gbc(gridx :1, gridy:4))
|
||||
|
||||
label(text : "Save downloaded files to:", constraints: gbc(gridx:0, gridy:5))
|
||||
button(text : "Choose", constraints : gbc(gridx : 1, gridy:5), downloadLocationAction)
|
||||
label(text : bind {model.downloadLocation}, constraints: gbc(gridx:0, gridy:6, gridwidth:2))
|
||||
label(text : "Share hidden files", constraints : gbc(gridx : 0, gridy:5))
|
||||
shareHiddenCheckbox = checkBox(selected : bind {model.shareHiddenFiles}, constraints : gbc(gridx :1, gridy:5))
|
||||
|
||||
label(text : "Allow browsing", constraints : gbc(gridx : 0, gridy : 6))
|
||||
browseFilesCheckbox = checkBox(selected : bind {model.browseFiles}, constraints : gbc(gridx : 1, gridy : 6))
|
||||
|
||||
label(text : "Save downloaded files to:", constraints: gbc(gridx:0, gridy:7))
|
||||
button(text : "Choose", constraints : gbc(gridx : 1, gridy:7), downloadLocationAction)
|
||||
label(text : bind {model.downloadLocation}, constraints: gbc(gridx:0, gridy:8, gridwidth:2))
|
||||
|
||||
}
|
||||
i = builder.panel {
|
||||
@@ -112,19 +127,28 @@ class OptionsView {
|
||||
gridBagLayout()
|
||||
label(text : "Changing these settings requires a restart", constraints : gbc(gridx : 0, gridy : 0, gridwidth: 2))
|
||||
label(text : "Look And Feel", constraints : gbc(gridx: 0, gridy:1))
|
||||
lnfField = textField(text : bind {model.lnf}, columns : 4, constraints : gbc(gridx : 1, gridy : 1))
|
||||
lnfField = textField(text : bind {model.lnf}, columns : 4, constraints : gbc(gridx : 1, gridy : 1, anchor : GridBagConstraints.LINE_START))
|
||||
label(text : "Font", constraints : gbc(gridx: 0, gridy : 2))
|
||||
fontField = textField(text : bind {model.font}, columns : 4, constraints : gbc(gridx : 1, gridy:2))
|
||||
// label(text : "Show Monitor", constraints : gbc(gridx :0, gridy: 3))
|
||||
// monitorCheckbox = checkBox(selected : bind {model.showMonitor}, constraints : gbc(gridx : 1, gridy: 3))
|
||||
label(text : "Clear Cancelled Downloads", constraints: gbc(gridx: 0, gridy:4))
|
||||
clearCancelledDownloadsCheckbox = checkBox(selected : bind {model.clearCancelledDownloads}, constraints : gbc(gridx : 1, gridy:4))
|
||||
label(text : "Clear Finished Downloads", constraints: gbc(gridx: 0, gridy:5))
|
||||
clearFinishedDownloadsCheckbox = checkBox(selected : bind {model.clearFinishedDownloads}, constraints : gbc(gridx : 1, gridy:5))
|
||||
label(text : "Exclude Local Files From Results", constraints: gbc(gridx:0, gridy:6))
|
||||
excludeLocalResultCheckbox = checkBox(selected : bind {model.excludeLocalResult}, constraints : gbc(gridx: 1, gridy : 6))
|
||||
// label(text : "Show Hash Searches In Monitor", constraints: gbc(gridx:0, gridy:7))
|
||||
// showSearchHashesCheckbox = checkBox(selected : bind {model.showSearchHashes}, constraints : gbc(gridx: 1, gridy: 7))
|
||||
fontField = textField(text : bind {model.font}, columns : 4, constraints : gbc(gridx : 1, gridy:2, anchor : GridBagConstraints.LINE_START))
|
||||
|
||||
label(text : "Font Size", constraints : gbc(gridx: 0, gridy : 3))
|
||||
buttonGroup(id: "fontSizeGroup")
|
||||
radioButton(text: "Automatic", selected : bind {model.automaticFontSize}, buttonGroup : fontSizeGroup,
|
||||
constraints : gbc(gridx : 1, gridy: 3, anchor : GridBagConstraints.LINE_START), automaticFontAction)
|
||||
radioButton(text: "Custom", selected : bind {!model.automaticFontSize}, buttonGroup : fontSizeGroup,
|
||||
constraints : gbc(gridx : 1, gridy: 4, anchor : GridBagConstraints.LINE_START), customFontAction)
|
||||
fontSizeField = textField(text : bind {model.customFontSize}, enabled : bind {!model.automaticFontSize}, constraints : gbc(gridx : 2, gridy : 4))
|
||||
|
||||
label(text : "Automatically Clear Cancelled Downloads", constraints: gbc(gridx: 0, gridy:5))
|
||||
clearCancelledDownloadsCheckbox = checkBox(selected : bind {model.clearCancelledDownloads},
|
||||
constraints : gbc(gridx : 1, gridy:5, anchor : GridBagConstraints.LINE_START))
|
||||
label(text : "Automatically Clear Finished Downloads", constraints: gbc(gridx: 0, gridy:6))
|
||||
clearFinishedDownloadsCheckbox = checkBox(selected : bind {model.clearFinishedDownloads},
|
||||
constraints : gbc(gridx : 1, gridy:6, anchor : GridBagConstraints.LINE_START))
|
||||
label(text : "Exclude Local Files From Results", constraints: gbc(gridx:0, gridy:7))
|
||||
excludeLocalResultCheckbox = checkBox(selected : bind {model.excludeLocalResult},
|
||||
constraints : gbc(gridx: 1, gridy : 7, anchor : GridBagConstraints.LINE_START))
|
||||
|
||||
}
|
||||
bandwidth = builder.panel {
|
||||
gridBagLayout()
|
||||
@@ -134,6 +158,18 @@ class OptionsView {
|
||||
label(text : "Outbound bandwidth (KB)", constraints : gbc(gridx: 0, gridy : 2))
|
||||
outBwField = textField(text : bind {model.outBw}, columns : 3, constraints : gbc(gridx : 1, gridy : 2))
|
||||
}
|
||||
trust = builder.panel {
|
||||
gridBagLayout()
|
||||
label(text : "Allow only trusted connections", constraints : gbc(gridx: 0, gridy : 0))
|
||||
allowUntrustedCheckbox = checkBox(selected : bind {model.onlyTrusted}, constraints : gbc(gridx: 1, gridy : 0))
|
||||
label(text : "Search extra hop", constraints : gbc(gridx:0, gridy:1))
|
||||
searchExtraHopCheckbox = checkBox(selected : bind {model.searchExtraHop}, constraints : gbc(gridx: 1, gridy : 1))
|
||||
label(text : "Allow others to view my trust list", constraints : gbc(gridx: 0, gridy : 2))
|
||||
allowTrustListsCheckbox = checkBox(selected : bind {model.trustLists}, constraints : gbc(gridx: 1, gridy : 2))
|
||||
label(text : "Update trust lists every ", constraints : gbc(gridx:0, gridy:3))
|
||||
trustListIntervalField = textField(text : bind {model.trustListInterval}, constraints:gbc(gridx:1, gridy:3))
|
||||
label(text : "hours", constraints : gbc(gridx: 2, gridy:3))
|
||||
}
|
||||
|
||||
|
||||
buttonsPanel = builder.panel {
|
||||
@@ -152,6 +188,7 @@ class OptionsView {
|
||||
if (core.router != null) {
|
||||
tabbedPane.addTab("Bandwidth", bandwidth)
|
||||
}
|
||||
tabbedPane.addTab("Trust", trust)
|
||||
|
||||
JPanel panel = new JPanel()
|
||||
panel.setLayout(new BorderLayout())
|
||||
|
183
gui/griffon-app/views/com/muwire/gui/OptionsView.groovy.orig
Normal file
183
gui/griffon-app/views/com/muwire/gui/OptionsView.groovy.orig
Normal file
@@ -0,0 +1,183 @@
|
||||
package com.muwire.gui
|
||||
|
||||
import griffon.core.artifact.GriffonView
|
||||
import griffon.inject.MVCMember
|
||||
import griffon.metadata.ArtifactProviderFor
|
||||
|
||||
import javax.swing.JDialog
|
||||
import javax.swing.JPanel
|
||||
import javax.swing.JTabbedPane
|
||||
import javax.swing.SwingConstants
|
||||
|
||||
import com.muwire.core.Core
|
||||
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.event.WindowAdapter
|
||||
import java.awt.event.WindowEvent
|
||||
|
||||
import javax.annotation.Nonnull
|
||||
|
||||
@ArtifactProviderFor(GriffonView)
|
||||
class OptionsView {
|
||||
@MVCMember @Nonnull
|
||||
FactoryBuilderSupport builder
|
||||
@MVCMember @Nonnull
|
||||
OptionsModel model
|
||||
|
||||
def d
|
||||
def p
|
||||
def i
|
||||
def u
|
||||
def bandwidth
|
||||
def trust
|
||||
|
||||
def retryField
|
||||
def updateField
|
||||
def autoDownloadUpdateCheckbox
|
||||
def shareDownloadedCheckbox
|
||||
|
||||
def inboundLengthField
|
||||
def inboundQuantityField
|
||||
def outboundLengthField
|
||||
def outboundQuantityField
|
||||
def i2pUDPPortField
|
||||
def i2pNTCPPortField
|
||||
|
||||
def lnfField
|
||||
def monitorCheckbox
|
||||
def fontField
|
||||
def clearCancelledDownloadsCheckbox
|
||||
def clearFinishedDownloadsCheckbox
|
||||
def excludeLocalResultCheckbox
|
||||
def showSearchHashesCheckbox
|
||||
|
||||
def inBwField
|
||||
def outBwField
|
||||
|
||||
def allowUntrustedCheckbox
|
||||
def allowTrustListsCheckbox
|
||||
def trustListIntervalField
|
||||
|
||||
def buttonsPanel
|
||||
|
||||
def mainFrame
|
||||
|
||||
void initUI() {
|
||||
mainFrame = application.windowManager.findWindow("main-frame")
|
||||
d = new JDialog(mainFrame, "Options", true)
|
||||
d.setResizable(false)
|
||||
p = builder.panel {
|
||||
gridBagLayout()
|
||||
label(text : "Retry failed downloads every", constraints : gbc(gridx: 0, gridy: 0))
|
||||
retryField = textField(text : bind { model.downloadRetryInterval }, columns : 2, constraints : gbc(gridx: 1, gridy: 0))
|
||||
label(text : "minutes", constraints : gbc(gridx : 2, gridy: 0))
|
||||
|
||||
label(text : "Check for updates every", constraints : gbc(gridx : 0, gridy: 1))
|
||||
updateField = textField(text : bind {model.updateCheckInterval }, columns : 2, constraints : gbc(gridx : 1, gridy: 1))
|
||||
label(text : "hours", constraints : gbc(gridx: 2, gridy : 1))
|
||||
|
||||
label(text : "Download updates automatically", constraints: gbc(gridx :0, gridy : 2))
|
||||
autoDownloadUpdateCheckbox = checkBox(selected : bind {model.autoDownloadUpdate}, constraints : gbc(gridx:1, gridy : 2))
|
||||
|
||||
label(text : "Share downloaded files", constraints : gbc(gridx : 0, gridy:3))
|
||||
shareDownloadedCheckbox = checkBox(selected : bind {model.shareDownloadedFiles}, constraints : gbc(gridx :1, gridy:3))
|
||||
|
||||
label(text : "Save downloaded files to:", constraints: gbc(gridx:0, gridy:4))
|
||||
button(text : "Choose", constraints : gbc(gridx : 1, gridy:4), downloadLocationAction)
|
||||
label(text : bind {model.downloadLocation}, constraints: gbc(gridx:0, gridy:5, gridwidth:2))
|
||||
|
||||
}
|
||||
i = builder.panel {
|
||||
gridBagLayout()
|
||||
label(text : "Changing these settings requires a restart", constraints : gbc(gridx : 0, gridy : 0, gridwidth: 2))
|
||||
label(text : "Inbound Length", constraints : gbc(gridx:0, gridy:1))
|
||||
inboundLengthField = textField(text : bind {model.inboundLength}, columns : 2, constraints : gbc(gridx:1, gridy:1))
|
||||
label(text : "Inbound Quantity", constraints : gbc(gridx:0, gridy:2))
|
||||
inboundQuantityField = textField(text : bind {model.inboundQuantity}, columns : 2, constraints : gbc(gridx:1, gridy:2))
|
||||
label(text : "Outbound Length", constraints : gbc(gridx:0, gridy:3))
|
||||
outboundLengthField = textField(text : bind {model.outboundLength}, columns : 2, constraints : gbc(gridx:1, gridy:3))
|
||||
label(text : "Outbound Quantity", constraints : gbc(gridx:0, gridy:4))
|
||||
outboundQuantityField = textField(text : bind {model.outboundQuantity}, columns : 2, constraints : gbc(gridx:1, gridy:4))
|
||||
|
||||
Core core = application.context.get("core")
|
||||
if (core.router != null) {
|
||||
label(text : "TCP Port", constraints : gbc(gridx :0, gridy: 5))
|
||||
i2pNTCPPortField = textField(text : bind {model.i2pNTCPPort}, columns : 4, constraints : gbc(gridx:1, gridy:5))
|
||||
label(text : "UDP Port", constraints : gbc(gridx :0, gridy: 6))
|
||||
i2pUDPPortField = textField(text : bind {model.i2pUDPPort}, columns : 4, constraints : gbc(gridx:1, gridy:6))
|
||||
}
|
||||
|
||||
}
|
||||
u = builder.panel {
|
||||
gridBagLayout()
|
||||
label(text : "Changing these settings requires a restart", constraints : gbc(gridx : 0, gridy : 0, gridwidth: 2))
|
||||
label(text : "Look And Feel", constraints : gbc(gridx: 0, gridy:1))
|
||||
lnfField = textField(text : bind {model.lnf}, columns : 4, constraints : gbc(gridx : 1, gridy : 1))
|
||||
label(text : "Font", constraints : gbc(gridx: 0, gridy : 2))
|
||||
fontField = textField(text : bind {model.font}, columns : 4, constraints : gbc(gridx : 1, gridy:2))
|
||||
// label(text : "Show Monitor", constraints : gbc(gridx :0, gridy: 3))
|
||||
// monitorCheckbox = checkBox(selected : bind {model.showMonitor}, constraints : gbc(gridx : 1, gridy: 3))
|
||||
label(text : "Clear Cancelled Downloads", constraints: gbc(gridx: 0, gridy:4))
|
||||
clearCancelledDownloadsCheckbox = checkBox(selected : bind {model.clearCancelledDownloads}, constraints : gbc(gridx : 1, gridy:4))
|
||||
label(text : "Clear Finished Downloads", constraints: gbc(gridx: 0, gridy:5))
|
||||
clearFinishedDownloadsCheckbox = checkBox(selected : bind {model.clearFinishedDownloads}, constraints : gbc(gridx : 1, gridy:5))
|
||||
label(text : "Exclude Local Files From Results", constraints: gbc(gridx:0, gridy:6))
|
||||
excludeLocalResultCheckbox = checkBox(selected : bind {model.excludeLocalResult}, constraints : gbc(gridx: 1, gridy : 6))
|
||||
// label(text : "Show Hash Searches In Monitor", constraints: gbc(gridx:0, gridy:7))
|
||||
// showSearchHashesCheckbox = checkBox(selected : bind {model.showSearchHashes}, constraints : gbc(gridx: 1, gridy: 7))
|
||||
}
|
||||
bandwidth = builder.panel {
|
||||
gridBagLayout()
|
||||
label(text : "Changing these settings requires a restart", constraints : gbc(gridx : 0, gridy : 0, gridwidth: 2))
|
||||
label(text : "Inbound bandwidth (KB)", constraints : gbc(gridx: 0, gridy : 1))
|
||||
inBwField = textField(text : bind {model.inBw}, columns : 3, constraints : gbc(gridx : 1, gridy : 1))
|
||||
label(text : "Outbound bandwidth (KB)", constraints : gbc(gridx: 0, gridy : 2))
|
||||
outBwField = textField(text : bind {model.outBw}, columns : 3, constraints : gbc(gridx : 1, gridy : 2))
|
||||
}
|
||||
trust = builder.panel {
|
||||
gridBagLayout()
|
||||
label(text : "Allow only trusted connections", constraints : gbc(gridx: 0, gridy : 0))
|
||||
allowUntrustedCheckbox = checkBox(selected : bind {model.onlyTrusted}, constraints : gbc(gridx: 1, gridy : 0))
|
||||
label(text : "Allow others to view my trust list", constraints : gbc(gridx: 0, gridy : 1))
|
||||
allowTrustListsCheckbox = checkBox(selected : bind {model.trustLists}, constraints : gbc(gridx: 1, gridy : 1))
|
||||
label(text : "Update trust lists every ", constraints : gbc(gridx:0, gridy:2))
|
||||
trustListIntervalField = textField(text : bind {model.trustListInterval}, constraints:gbc(gridx:1, gridy:2))
|
||||
label(text : "hours", constraints : gbc(gridx: 2, gridy:2))
|
||||
}
|
||||
|
||||
|
||||
buttonsPanel = builder.panel {
|
||||
gridBagLayout()
|
||||
button(text : "Save", constraints : gbc(gridx : 1, gridy: 2), saveAction)
|
||||
button(text : "Cancel", constraints : gbc(gridx : 2, gridy: 2), cancelAction)
|
||||
}
|
||||
}
|
||||
|
||||
void mvcGroupInit(Map<String,String> args) {
|
||||
def tabbedPane = new JTabbedPane()
|
||||
tabbedPane.addTab("MuWire", p)
|
||||
tabbedPane.addTab("I2P", i)
|
||||
tabbedPane.addTab("GUI", u)
|
||||
Core core = application.context.get("core")
|
||||
if (core.router != null) {
|
||||
tabbedPane.addTab("Bandwidth", bandwidth)
|
||||
}
|
||||
tabbedPane.addTab("Trust", trust)
|
||||
|
||||
JPanel panel = new JPanel()
|
||||
panel.setLayout(new BorderLayout())
|
||||
panel.add(tabbedPane, BorderLayout.CENTER)
|
||||
panel.add(buttonsPanel, BorderLayout.SOUTH)
|
||||
|
||||
d.getContentPane().add(panel)
|
||||
d.pack()
|
||||
d.setLocationRelativeTo(mainFrame)
|
||||
d.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE)
|
||||
d.addWindowListener(new WindowAdapter() {
|
||||
public void windowClosed(WindowEvent e) {
|
||||
mvcGroup.destroy()
|
||||
}
|
||||
})
|
||||
d.show()
|
||||
}
|
||||
}
|
@@ -11,15 +11,20 @@ import javax.swing.JComponent
|
||||
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.table.DefaultTableCellRenderer
|
||||
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.search.UIResultEvent
|
||||
import com.muwire.core.util.DataUtil
|
||||
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.Color
|
||||
import java.awt.FlowLayout
|
||||
import java.awt.GridBagConstraints
|
||||
import java.awt.Toolkit
|
||||
import java.awt.datatransfer.StringSelection
|
||||
import java.awt.event.MouseAdapter
|
||||
@@ -37,23 +42,72 @@ class SearchTabView {
|
||||
def pane
|
||||
def parent
|
||||
def searchTerms
|
||||
def sendersTable
|
||||
def lastSendersSortEvent
|
||||
def resultsTable
|
||||
def lastSortEvent
|
||||
def sequentialDownloadCheckbox
|
||||
|
||||
void initUI() {
|
||||
builder.with {
|
||||
def resultsTable
|
||||
def pane = scrollPane {
|
||||
def sendersTable
|
||||
def sequentialDownloadCheckbox
|
||||
def pane = panel {
|
||||
gridLayout(rows :1, cols : 1)
|
||||
splitPane(orientation: JSplitPane.VERTICAL_SPLIT, continuousLayout : true, dividerLocation: 300 ) {
|
||||
panel {
|
||||
borderLayout()
|
||||
scrollPane (constraints : BorderLayout.CENTER) {
|
||||
sendersTable = table(id : "senders-table", autoCreateRowSorter : true) {
|
||||
tableModel(list : model.senders) {
|
||||
closureColumn(header : "Sender", preferredWidth : 500, type: String, read : {row -> row.getHumanReadableName()})
|
||||
closureColumn(header : "Results", preferredWidth : 20, type: Integer, read : {row -> model.sendersBucket[row].size()})
|
||||
closureColumn(header : "Browse", preferredWidth : 20, type: Boolean, read : {row -> model.sendersBucket[row].first().browse})
|
||||
closureColumn(header : "Trust", preferredWidth : 50, type: String, read : { row ->
|
||||
model.core.trustService.getLevel(row.destination).toString()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
panel(constraints : BorderLayout.SOUTH) {
|
||||
gridLayout(rows: 1, cols : 2)
|
||||
panel {
|
||||
button(text : "Browse Host", enabled : bind {model.browseActionEnabled}, browseAction)
|
||||
}
|
||||
panel {
|
||||
button(text : "Trust", enabled: bind {model.trustButtonsEnabled }, trustAction)
|
||||
button(text : "Neutral", enabled: bind {model.trustButtonsEnabled}, neutralAction)
|
||||
button(text : "Distrust", enabled : bind {model.trustButtonsEnabled}, distrustAction)
|
||||
}
|
||||
}
|
||||
}
|
||||
panel {
|
||||
borderLayout()
|
||||
scrollPane (constraints : BorderLayout.CENTER) {
|
||||
resultsTable = table(id : "results-table", autoCreateRowSorter : true) {
|
||||
tableModel(list: model.results) {
|
||||
closureColumn(header: "Name", preferredWidth: 350, type: String, read : {row -> row.name.replace('<','_')})
|
||||
closureColumn(header: "Size", preferredWidth: 20, type: Long, read : {row -> row.size})
|
||||
closureColumn(header: "Direct Sources", preferredWidth: 50, type : Integer, read : { row -> model.hashBucket[row.infohash].size()})
|
||||
closureColumn(header: "Possible Sources", preferredWidth : 50, type : Integer, read : {row -> model.sourcesBucket[row.infohash].size()})
|
||||
closureColumn(header: "Sender", preferredWidth: 170, type: String, read : {row -> row.sender.getHumanReadableName()})
|
||||
closureColumn(header: "Trust", preferredWidth: 50, type: String, read : {row ->
|
||||
model.core.trustService.getLevel(row.sender.destination).toString()
|
||||
})
|
||||
closureColumn(header: "Comments", preferredWidth: 20, type: Boolean, read : {row -> row.comment != null})
|
||||
}
|
||||
}
|
||||
}
|
||||
panel(constraints : BorderLayout.SOUTH) {
|
||||
gridLayout(rows: 1, cols: 3)
|
||||
panel()
|
||||
panel {
|
||||
button(text : "Download", enabled : bind {model.downloadActionEnabled}, downloadAction)
|
||||
}
|
||||
panel {
|
||||
gridBagLayout()
|
||||
panel (constraints : gbc(gridx : 0, gridy : 0, weightx : 100))
|
||||
sequentialDownloadCheckbox = checkBox(constraints : gbc(gridx : 1, gridy: 0, weightx : 0),selected : false, enabled : bind {model.downloadActionEnabled})
|
||||
label(constraints: gbc(gridx: 2, gridy: 0, weightx : 0),text : "Download sequentially")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -63,17 +117,27 @@ class SearchTabView {
|
||||
this.pane.putClientProperty("results-table",resultsTable)
|
||||
|
||||
this.resultsTable = resultsTable
|
||||
this.sendersTable = sendersTable
|
||||
this.sequentialDownloadCheckbox = sequentialDownloadCheckbox
|
||||
|
||||
def selectionModel = resultsTable.getSelectionModel()
|
||||
selectionModel.setSelectionMode(ListSelectionModel.SINGLE_SELECTION)
|
||||
selectionModel.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION)
|
||||
selectionModel.addListSelectionListener( {
|
||||
int row = resultsTable.getSelectedRow()
|
||||
if (row < 0)
|
||||
int[] rows = resultsTable.getSelectedRows()
|
||||
if (rows.length == 0) {
|
||||
model.downloadActionEnabled = false
|
||||
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)
|
||||
}
|
||||
if (lastSortEvent != null) {
|
||||
for (int i = 0; i < rows.length; i ++) {
|
||||
rows[i] = resultsTable.rowSorter.convertRowIndexToModel(rows[i])
|
||||
}
|
||||
}
|
||||
boolean downloadActionEnabled = true
|
||||
rows.each {
|
||||
downloadActionEnabled &= mvcGroup.parentGroup.model.canDownload(model.results[it].infohash)
|
||||
}
|
||||
model.downloadActionEnabled = downloadActionEnabled
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -98,12 +162,11 @@ class SearchTabView {
|
||||
}
|
||||
|
||||
parent.setTabComponentAt(index, tabPanel)
|
||||
mvcGroup.parentGroup.view.showSearchWindow.call()
|
||||
|
||||
def centerRenderer = new DefaultTableCellRenderer()
|
||||
centerRenderer.setHorizontalAlignment(JLabel.CENTER)
|
||||
resultsTable.columnModel.getColumn(1).setCellRenderer(centerRenderer)
|
||||
resultsTable.setDefaultRenderer(Integer.class,centerRenderer)
|
||||
resultsTable.columnModel.getColumn(4).setCellRenderer(centerRenderer)
|
||||
|
||||
resultsTable.columnModel.getColumn(1).setCellRenderer(new SizeRenderer())
|
||||
|
||||
@@ -118,7 +181,7 @@ class SearchTabView {
|
||||
if (e.button == MouseEvent.BUTTON3)
|
||||
showPopupMenu(e)
|
||||
else if (e.button == MouseEvent.BUTTON1 && e.clickCount == 2)
|
||||
mvcGroup.parentGroup.controller.download()
|
||||
mvcGroup.controller.download()
|
||||
}
|
||||
@Override
|
||||
public void mouseReleased(MouseEvent e) {
|
||||
@@ -126,38 +189,123 @@ class SearchTabView {
|
||||
showPopupMenu(e)
|
||||
}
|
||||
})
|
||||
|
||||
// senders table
|
||||
sendersTable.setDefaultRenderer(Integer.class, centerRenderer)
|
||||
sendersTable.rowSorter.addRowSorterListener({evt -> lastSendersSortEvent = evt})
|
||||
sendersTable.rowSorter.setSortsOnUpdates(true)
|
||||
def selectionModel = sendersTable.getSelectionModel()
|
||||
selectionModel.setSelectionMode(ListSelectionModel.SINGLE_SELECTION)
|
||||
selectionModel.addListSelectionListener({
|
||||
int row = selectedSenderRow()
|
||||
if (row < 0) {
|
||||
model.trustButtonsEnabled = false
|
||||
model.browseActionEnabled = false
|
||||
return
|
||||
} else {
|
||||
Persona sender = model.senders[row]
|
||||
model.browseActionEnabled = model.sendersBucket[sender].first().browse
|
||||
model.trustButtonsEnabled = true
|
||||
model.results.clear()
|
||||
model.results.addAll(model.sendersBucket[sender])
|
||||
resultsTable.model.fireTableDataChanged()
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
def closeTab = {
|
||||
int index = parent.indexOfTab(searchTerms)
|
||||
parent.removeTabAt(index)
|
||||
mvcGroup.parentGroup.model.trustButtonsEnabled = false
|
||||
mvcGroup.parentGroup.model.downloadActionEnabled = false
|
||||
model.trustButtonsEnabled = false
|
||||
model.downloadActionEnabled = false
|
||||
mvcGroup.destroy()
|
||||
}
|
||||
|
||||
def showPopupMenu(MouseEvent e) {
|
||||
JPopupMenu menu = new JPopupMenu()
|
||||
if (mvcGroup.parentGroup.model.downloadActionEnabled) {
|
||||
boolean showMenu = false
|
||||
if (model.downloadActionEnabled) {
|
||||
JMenuItem download = new JMenuItem("Download")
|
||||
download.addActionListener({mvcGroup.parentGroup.controller.download()})
|
||||
download.addActionListener({mvcGroup.controller.download()})
|
||||
menu.add(download)
|
||||
showMenu = true
|
||||
}
|
||||
if (resultsTable.getSelectedRows().length == 1) {
|
||||
JMenuItem copyHashToClipboard = new JMenuItem("Copy hash to clipboard")
|
||||
copyHashToClipboard.addActionListener({mvcGroup.view.copyHashToClipboard()})
|
||||
menu.add(copyHashToClipboard)
|
||||
JMenuItem copyNameToClipboard = new JMenuItem("Copy name to clipboard")
|
||||
copyNameToClipboard.addActionListener({mvcGroup.view.copyNameToClipboard()})
|
||||
menu.add(copyNameToClipboard)
|
||||
showMenu = true
|
||||
|
||||
// show comment if any
|
||||
int selectedRow = resultsTable.getSelectedRow()
|
||||
if (lastSortEvent != null)
|
||||
selectedRow = resultsTable.rowSorter.convertRowIndexToModel(selectedRow)
|
||||
if (model.results[selectedRow].comment != null) {
|
||||
JMenuItem showComment = new JMenuItem("Show Comment")
|
||||
showComment.addActionListener({mvcGroup.view.showComment()})
|
||||
menu.add(showComment)
|
||||
}
|
||||
}
|
||||
if (showMenu)
|
||||
menu.show(e.getComponent(), e.getX(), e.getY())
|
||||
}
|
||||
|
||||
def copyHashToClipboard() {
|
||||
int selected = resultsTable.getSelectedRow()
|
||||
if (selected < 0)
|
||||
return
|
||||
private UIResultEvent getSelectedResult() {
|
||||
int[] selectedRows = resultsTable.getSelectedRows()
|
||||
if (selectedRows.length != 1)
|
||||
return null
|
||||
int selected = selectedRows[0]
|
||||
if (lastSortEvent != null)
|
||||
selected = resultsTable.rowSorter.convertRowIndexToModel(selected)
|
||||
String hash = Base64.encode(model.results[selected].infohash.getRoot())
|
||||
model.results[selected]
|
||||
}
|
||||
|
||||
def copyHashToClipboard() {
|
||||
def result = getSelectedResult()
|
||||
if (result == null)
|
||||
return
|
||||
String hash = Base64.encode(result.infohash.getRoot())
|
||||
StringSelection selection = new StringSelection(hash)
|
||||
def clipboard = Toolkit.getDefaultToolkit().getSystemClipboard()
|
||||
clipboard.setContents(selection, null)
|
||||
}
|
||||
|
||||
def copyNameToClipboard() {
|
||||
def result = getSelectedResult()
|
||||
if (result == null)
|
||||
return
|
||||
StringSelection selection = new StringSelection(result.getName())
|
||||
def clipboard = Toolkit.getDefaultToolkit().getSystemClipboard()
|
||||
clipboard.setContents(selection, null)
|
||||
}
|
||||
|
||||
def showComment() {
|
||||
int selectedRow = resultsTable.getSelectedRow()
|
||||
if (selectedRow < 0)
|
||||
return
|
||||
if (lastSortEvent != null)
|
||||
selectedRow = resultsTable.rowSorter.convertRowIndexToModel(selectedRow)
|
||||
UIResultEvent event = model.results[selectedRow]
|
||||
if (event.comment == null)
|
||||
return
|
||||
|
||||
String groupId = Base64.encode(event.infohash.getRoot())
|
||||
Map<String,Object> params = new HashMap<>()
|
||||
params['result'] = event
|
||||
|
||||
mvcGroup.createMVCGroup("show-comment", groupId, params)
|
||||
}
|
||||
|
||||
int selectedSenderRow() {
|
||||
int row = sendersTable.getSelectedRow()
|
||||
if (row < 0)
|
||||
return -1
|
||||
if (lastSendersSortEvent != null)
|
||||
row = sendersTable.rowSorter.convertRowIndexToModel(row)
|
||||
row
|
||||
}
|
||||
}
|
61
gui/griffon-app/views/com/muwire/gui/ShowCommentView.groovy
Normal file
61
gui/griffon-app/views/com/muwire/gui/ShowCommentView.groovy
Normal file
@@ -0,0 +1,61 @@
|
||||
package com.muwire.gui
|
||||
|
||||
import griffon.core.artifact.GriffonView
|
||||
import griffon.inject.MVCMember
|
||||
import griffon.metadata.ArtifactProviderFor
|
||||
import net.i2p.data.Base64
|
||||
|
||||
import javax.swing.JDialog
|
||||
import javax.swing.SwingConstants
|
||||
|
||||
import com.muwire.core.util.DataUtil
|
||||
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.event.WindowAdapter
|
||||
import java.awt.event.WindowEvent
|
||||
|
||||
import javax.annotation.Nonnull
|
||||
|
||||
@ArtifactProviderFor(GriffonView)
|
||||
class ShowCommentView {
|
||||
@MVCMember @Nonnull
|
||||
FactoryBuilderSupport builder
|
||||
@MVCMember @Nonnull
|
||||
ShowCommentModel model
|
||||
|
||||
def mainFrame
|
||||
def dialog
|
||||
def p
|
||||
|
||||
void initUI() {
|
||||
mainFrame = application.windowManager.findWindow("main-frame")
|
||||
dialog = new JDialog(mainFrame, model.result.name, true)
|
||||
dialog.setResizable(true)
|
||||
|
||||
|
||||
p = builder.panel {
|
||||
borderLayout()
|
||||
panel (constraints : BorderLayout.CENTER) {
|
||||
scrollPane {
|
||||
textArea(text : model.result.comment, rows : 20, columns : 100, editable : false)
|
||||
}
|
||||
}
|
||||
panel (constraints : BorderLayout.SOUTH) {
|
||||
button(text : "Dismiss", dismissAction)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void mvcGroupInit(Map<String,String> args) {
|
||||
dialog.getContentPane().add(p)
|
||||
dialog.pack()
|
||||
dialog.setLocationRelativeTo(mainFrame)
|
||||
dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE)
|
||||
dialog.addWindowListener( new WindowAdapter() {
|
||||
public void windowClosed(WindowEvent e) {
|
||||
mvcGroup.destroy()
|
||||
}
|
||||
})
|
||||
dialog.show()
|
||||
}
|
||||
}
|
120
gui/griffon-app/views/com/muwire/gui/TrustListView.groovy
Normal file
120
gui/griffon-app/views/com/muwire/gui/TrustListView.groovy
Normal file
@@ -0,0 +1,120 @@
|
||||
package com.muwire.gui
|
||||
|
||||
import griffon.core.artifact.GriffonView
|
||||
import griffon.inject.MVCMember
|
||||
import griffon.metadata.ArtifactProviderFor
|
||||
|
||||
import javax.swing.JDialog
|
||||
import javax.swing.ListSelectionModel
|
||||
import javax.swing.SwingConstants
|
||||
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.event.WindowAdapter
|
||||
import java.awt.event.WindowEvent
|
||||
|
||||
import javax.annotation.Nonnull
|
||||
|
||||
@ArtifactProviderFor(GriffonView)
|
||||
class TrustListView {
|
||||
@MVCMember @Nonnull
|
||||
FactoryBuilderSupport builder
|
||||
@MVCMember @Nonnull
|
||||
TrustListModel model
|
||||
|
||||
def dialog
|
||||
def mainFrame
|
||||
def mainPanel
|
||||
|
||||
def sortEvents = [:]
|
||||
|
||||
void initUI() {
|
||||
mainFrame = application.windowManager.findWindow("main-frame")
|
||||
dialog = new JDialog(mainFrame, model.trustList.persona.getHumanReadableName(), true)
|
||||
mainPanel = builder.panel {
|
||||
borderLayout()
|
||||
panel(constraints : BorderLayout.NORTH) {
|
||||
borderLayout()
|
||||
panel (constraints : BorderLayout.NORTH) {
|
||||
label(text: "Trust List of "+model.trustList.persona.getHumanReadableName())
|
||||
}
|
||||
panel (constraints: BorderLayout.SOUTH) {
|
||||
label(text : "Last updated "+ new Date(model.trustList.timestamp))
|
||||
}
|
||||
}
|
||||
panel(constraints : BorderLayout.CENTER) {
|
||||
gridLayout(rows : 1, cols : 2)
|
||||
panel {
|
||||
borderLayout()
|
||||
scrollPane (constraints : BorderLayout.CENTER){
|
||||
table(id : "trusted-table", autoCreateRowSorter : true) {
|
||||
tableModel(list : model.trusted) {
|
||||
closureColumn(header: "Trusted Users", type : String, read : {it.getHumanReadableName()})
|
||||
closureColumn(header: "Your Trust", type : String, read : {model.trustService.getLevel(it.destination).toString()})
|
||||
}
|
||||
}
|
||||
}
|
||||
panel (constraints : BorderLayout.SOUTH) {
|
||||
gridBagLayout()
|
||||
button(text : "Trust", constraints : gbc(gridx : 0, gridy : 0), trustFromTrustedAction)
|
||||
button(text : "Distrust", constraints : gbc(gridx : 1, gridy : 0), distrustFromTrustedAction)
|
||||
}
|
||||
}
|
||||
panel {
|
||||
borderLayout()
|
||||
scrollPane (constraints : BorderLayout.CENTER ){
|
||||
table(id : "distrusted-table", autoCreateRowSorter : true) {
|
||||
tableModel(list : model.distrusted) {
|
||||
closureColumn(header: "Distrusted Users", type : String, read : {it.getHumanReadableName()})
|
||||
closureColumn(header: "Your Trust", type : String, read : {model.trustService.getLevel(it.destination).toString()})
|
||||
}
|
||||
}
|
||||
}
|
||||
panel(constraints : BorderLayout.SOUTH) {
|
||||
gridBagLayout()
|
||||
button(text : "Trust", constraints : gbc(gridx : 0, gridy : 0), trustFromDistrustedAction)
|
||||
button(text : "Distrust", constraints : gbc(gridx : 1, gridy : 0), distrustFromDistrustedAction)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void mvcGroupInit(Map<String,String> args) {
|
||||
|
||||
def trustedTable = builder.getVariable("trusted-table")
|
||||
trustedTable.rowSorter.addRowSorterListener({evt -> sortEvents["trusted-table"] = evt})
|
||||
trustedTable.rowSorter.setSortsOnUpdates(true)
|
||||
trustedTable.getSelectionModel().setSelectionMode(ListSelectionModel.SINGLE_SELECTION)
|
||||
|
||||
def distrustedTable = builder.getVariable("distrusted-table")
|
||||
distrustedTable.rowSorter.addRowSorterListener({evt -> sortEvents["distrusted-table"] = evt})
|
||||
distrustedTable.rowSorter.setSortsOnUpdates(true)
|
||||
distrustedTable.getSelectionModel().setSelectionMode(ListSelectionModel.SINGLE_SELECTION)
|
||||
|
||||
dialog.getContentPane().add(mainPanel)
|
||||
dialog.pack()
|
||||
dialog.setLocationRelativeTo(mainFrame)
|
||||
dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE)
|
||||
dialog.addWindowListener(new WindowAdapter() {
|
||||
public void windowClosed(WindowEvent e) {
|
||||
mvcGroup.destroy()
|
||||
}
|
||||
})
|
||||
dialog.show()
|
||||
}
|
||||
|
||||
int getSelectedRow(String tableName) {
|
||||
def table = builder.getVariable(tableName)
|
||||
int selectedRow = table.getSelectedRow()
|
||||
if (selectedRow < 0)
|
||||
return -1
|
||||
if (sortEvents.get(tableName) != null)
|
||||
selectedRow = table.rowSorter.convertRowIndexToModel(selectedRow)
|
||||
selectedRow
|
||||
}
|
||||
|
||||
void fireUpdate(String tableName) {
|
||||
def table = builder.getVariable(tableName)
|
||||
table.model.fireTableDataChanged()
|
||||
}
|
||||
}
|
@@ -0,0 +1,25 @@
|
||||
package com.muwire.gui
|
||||
|
||||
import griffon.core.test.GriffonFestRule
|
||||
import org.fest.swing.fixture.FrameFixture
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
import static org.junit.Assert.fail
|
||||
|
||||
class ContentPanelIntegrationTest {
|
||||
static {
|
||||
System.setProperty('griffon.swing.edt.violations.check', 'true')
|
||||
System.setProperty('griffon.swing.edt.hang.monitor', 'true')
|
||||
}
|
||||
|
||||
@Rule
|
||||
public final GriffonFestRule fest = new GriffonFestRule()
|
||||
|
||||
private FrameFixture window
|
||||
|
||||
@Test
|
||||
void smokeTest() {
|
||||
fail('Not implemented yet!')
|
||||
}
|
||||
}
|
@@ -0,0 +1,25 @@
|
||||
package com.muwire.gui
|
||||
|
||||
import griffon.core.test.GriffonFestRule
|
||||
import org.fest.swing.fixture.FrameFixture
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
import static org.junit.Assert.fail
|
||||
|
||||
class TrustListIntegrationTest {
|
||||
static {
|
||||
System.setProperty('griffon.swing.edt.violations.check', 'true')
|
||||
System.setProperty('griffon.swing.edt.hang.monitor', 'true')
|
||||
}
|
||||
|
||||
@Rule
|
||||
public final GriffonFestRule fest = new GriffonFestRule()
|
||||
|
||||
private FrameFixture window
|
||||
|
||||
@Test
|
||||
void smokeTest() {
|
||||
fail('Not implemented yet!')
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user