Compare commits

..

95 Commits

Author SHA1 Message Date
Zlatin Balevsky
54073af933 Release 0.5.2 2019-10-22 00:28:53 +01:00
Zlatin Balevsky
a32903fc8c prettier i2p status panel 2019-10-22 00:11:57 +01:00
Zlatin Balevsky
e40520be46 count hopeless and failing hosts, prettier status panel 2019-10-21 23:57:15 +01:00
Zlatin Balevsky
97482b949a de-capitalize for consistency 2019-10-21 22:50:21 +01:00
Zlatin Balevsky
92ee107312 remove duplicate variable 2019-10-21 22:23:29 +01:00
Zlatin Balevsky
2e8082af64 use titled borders everywhere for consistency 2019-10-21 22:12:39 +01:00
Zlatin Balevsky
8da5a428c9 make the i2p version a variable 2019-10-21 21:02:37 +01:00
Zlatin Balevsky
fd46b3c7d6 do not display fractions in percentage 2019-10-21 20:37:30 +01:00
Zlatin Balevsky
eea3b2563b allign router-specific settings 2019-10-21 20:16:36 +01:00
Zlatin Balevsky
50719f3828 move settings to top of panel 2019-10-21 20:12:08 +01:00
Zlatin Balevsky
01a45a89a8 reorganize the options view 2019-10-21 19:44:33 +01:00
Zlatin Balevsky
66bd249ed3 show percentage of fetched results 2019-10-21 18:28:37 +01:00
Zlatin Balevsky
265cd6ee15 more accurate description 2019-10-20 20:19:47 +01:00
Zlatin Balevsky
1dc88cb96b make speed smoothing interval configurable 2019-10-20 20:09:24 +01:00
Zlatin Balevsky
3e10d497b1 add an ETA column to downloads table 2019-10-20 19:11:32 +01:00
Zlatin Balevsky
9a0b3bb9d6 fix download table selection when sorted 2019-10-20 18:47:48 +01:00
Zlatin Balevsky
a1fe3c01b9 if no incompletes are in serialized json, use the default one, assuming an upgrade 2019-10-20 18:24:16 +01:00
Zlatin Balevsky
ab323db62a add ability to choose the incompletes location 2019-10-20 18:16:07 +01:00
Zlatin Balevsky
d954387e41 fix showing of local files in results 2019-10-20 11:59:48 +01:00
Zlatin Balevsky
ea9db21a18 wip on compressed results 2019-10-20 01:01:34 +01:00
Zlatin Balevsky
136cf89c9b groovy != java 2019-10-20 00:55:31 +01:00
Zlatin Balevsky
46de1baf88 compressed results 2019-10-20 00:54:32 +01:00
Zlatin Balevsky
13f7b8563c fix a bug where disabled browsing was shown as browsable. Log the response code if it's not 200 2019-10-19 22:33:47 +01:00
Zlatin Balevsky
9c15208f3a Release 0.5.1 2019-10-19 19:11:04 +01:00
Zlatin Balevsky
a9ce9d96b3 wip on menu; close zlib stream 2019-10-19 18:54:58 +01:00
Zlatin Balevsky
4d2a5a8018 MainFrameModel doesn't need to listen to single result events anymore 2019-10-19 18:12:30 +01:00
Zlatin Balevsky
8395047386 compress results in browse connections 2019-10-19 17:59:08 +01:00
Zlatin Balevsky
cb23aa44f0 enable SEVERE log messages if no config file specified 2019-10-19 05:53:33 +01:00
Zlatin Balevsky
dbcb8508b8 add a view comment button 2019-10-19 05:35:04 +01:00
Zlatin Balevsky
47d406d93b add a border around the two panels 2019-10-19 04:59:37 +01:00
Zlatin Balevsky
e06f1805c2 redirect griffon logging to jul 2019-10-19 04:45:45 +01:00
Zlatin Balevsky
2b04374e23 add option to disable browsing of files, make the dialog bigger 2019-10-19 00:53:13 +01:00
Zlatin Balevsky
383addbc37 implement view comment from browse window 2019-10-19 00:30:03 +01:00
Zlatin Balevsky
cc39cd7f8e implement downloading from browse window 2019-10-19 00:23:43 +01:00
Zlatin Balevsky
83665d7524 wip on browse host 2019-10-18 23:55:07 +01:00
Zlatin Balevsky
94340480b4 wip on browse host 2019-10-18 23:25:26 +01:00
Zlatin Balevsky
8850d49c63 wip on browse host 2019-10-18 23:16:37 +01:00
Zlatin Balevsky
f0f9d840f0 wip on browse host 2019-10-18 22:35:17 +01:00
Zlatin Balevsky
7f4cd4f331 wip on browse host 2019-10-18 21:17:34 +01:00
Zlatin Balevsky
e6162503f6 wip on browse host 2019-10-18 20:29:39 +01:00
Zlatin Balevsky
7a5d71dc36 add copy name to clipboard option 2019-10-17 19:01:53 +01:00
Zlatin Balevsky
6fa39a5e35 turn off logging if there is no config file 2019-10-17 18:39:28 +01:00
Zlatin Balevsky
c5ae804f61 Implement automatic font sizing; set all font properties on change of font 2019-10-17 18:15:04 +01:00
Zlatin Balevsky
d7695b448d remove my DS_Store 2019-10-17 05:50:29 +01:00
Zlatin Balevsky
946d9c8f32 disable sharing of hidden files by default, add option to enable 2019-10-17 05:46:27 +01:00
Zlatin Balevsky
02441ca1e3 add option to disable searching in comments 2019-10-16 19:57:18 +01:00
Zlatin Balevsky
5fa21b2360 keep tree expanded on modifications 2019-10-16 14:42:40 +01:00
Zlatin Balevsky
d4c08f4fe6 only remove from index if no more files have the same comment pt.2 2019-10-16 14:23:12 +01:00
Zlatin Balevsky
942de287c6 only remove from index if no more files have the same comment 2019-10-16 14:21:50 +01:00
Zlatin Balevsky
d0299f80c6 search through comments 2019-10-16 14:06:11 +01:00
Zlatin Balevsky
1227cf9263 Release 0.5.0 2019-10-15 12:38:25 +01:00
Zlatin Balevsky
a05575485f move things around 2019-10-15 10:40:50 +01:00
Zlatin Balevsky
f5bccd8126 All shared directories are watched directories. Fix manipulation of tree structure 2019-10-15 08:38:23 +01:00
Zlatin Balevsky
70fb789abf remove the watched directories table 2019-10-15 04:51:21 +01:00
Zlatin Balevsky
feb712c253 Move persisting of files on dedicated thread. Introduce an event to forcefully persist files. Do that immediately after unsharing anything 2019-10-15 04:21:40 +01:00
Zlatin Balevsky
d22b403e2a stop watching multiple directories at once 2019-10-14 23:16:05 +01:00
Zlatin Balevsky
a24982e0df fix comments for local results 2019-10-14 22:47:52 +01:00
Zlatin Balevsky
6c26019164 allow switching without restart 2019-10-14 21:40:03 +01:00
Zlatin Balevsky
965fa79bbf fix count of shared files in tree view mode 2019-10-14 20:57:50 +01:00
Zlatin Balevsky
60ddb85461 Tree view of the shared files. The count is wrong for some reason 2019-10-14 20:13:25 +01:00
Zlatin Balevsky
c7284623bc Release 0.4.16 2019-10-13 22:14:33 +01:00
Zlatin Balevsky
3e7f2aa70a Add a note about DND, automatically watch shared directories 2019-10-13 20:21:28 +01:00
Zlatin Balevsky
4f436a636c implement drop on MW -> share files/directories 2019-10-13 20:00:08 +01:00
Zlatin Balevsky
b49dbc30c3 comment already decoded by the time it gets to the gui 2019-10-11 19:01:40 +01:00
Zlatin Balevsky
c25d314e1c typo 2019-10-11 18:56:46 +01:00
Zlatin Balevsky
b28587a275 wip on file comments 2019-10-11 18:42:02 +01:00
Zlatin Balevsky
8b8e5d59be Silence an IllegalArgumentException while sorting downloads table 2019-10-11 11:21:56 +01:00
Zlatin Balevsky
70bbe1f636 update version 2019-10-10 17:33:07 +01:00
Zlatin Balevsky
337605dc0f Release 0.4.15 2019-10-10 16:48:10 +01:00
Zlatin Balevsky
14bdfa6b2e throttle even further - 500/s 2019-10-09 17:34:54 +01:00
Zlatin Balevsky
ed3f9da773 throttle loading even further, to 1000/sec 2019-10-09 16:46:17 +01:00
Zlatin Balevsky
251080d08f throttle loading of files to 500/s 2019-10-09 16:34:09 +01:00
Zlatin Balevsky
f530ab999d operations on multiple selection in shared files table 2019-10-09 03:38:08 +01:00
Zlatin Balevsky
4133384e48 ability to share multiple files and directories 2019-10-08 21:30:34 +01:00
Zlatin Balevsky
600fc98868 update TODO 2019-10-07 12:38:26 +01:00
Zlatin Balevsky
129eeb3b88 JDK needed, not JRE 2019-10-07 12:38:09 +01:00
Zlatin Balevsky
20b51b78a0 reduce priority of file persister thread 2019-10-07 11:59:51 +01:00
Zlatin Balevsky
33fe755b60 implement multiple-selection on downloads table 2019-10-07 04:26:35 +01:00
Zlatin Balevsky
8b0668a134 Rewrite utils into Java, cache the persistable data of shared files to reduce object churn 2019-10-05 22:50:32 +01:00
Zlatin Balevsky
730d2202fd bundles for linux available now 2019-10-05 18:53:43 +01:00
Zlatin Balevsky
69906a986d set i2p.dir.base to prevent router creating files in PWD 2019-10-05 15:03:59 +01:00
Zlatin Balevsky
5bc8fa8633 Preserve selection on refresh #18 2019-10-05 05:13:49 +01:00
Zlatin Balevsky
7de7c9d8f3 Add 'Clear Hits' button to content control panel #18 2019-10-05 05:03:25 +01:00
Zlatin Balevsky
e943f6019d disable all GUI unit tests, enable host-cache unit tests. The 'build' target now succeeds 2019-10-05 04:31:11 +01:00
Zlatin Balevsky
2eec7bec5b fix most core tests 2019-10-05 04:20:14 +01:00
Zlatin Balevsky
c36110cf76 update readme 2019-10-04 16:41:07 +01:00
Zlatin Balevsky
abe28517bc Release 0.4.14 2019-10-04 13:00:57 +01:00
Zlatin Balevsky
15bc4c064d center the button 2019-10-03 21:32:32 +01:00
Zlatin Balevsky
91d771944b add option for sequential download 2019-10-03 20:45:22 +01:00
Zlatin Balevsky
e09c456a13 make the download retry interval in seconds, default still 1 minute 2019-10-03 19:31:15 +01:00
Zlatin Balevsky
d9c1067226 Add Neutral button to search tab, issue #17 2019-10-02 06:02:06 +01:00
Zlatin Balevsky
eda3e7ad3a Add option to not search extra hop, only considered if connecting only to trusted peers, issue #6 2019-10-02 05:45:46 +01:00
Zlatin Balevsky
e9798c7eaa remember last rejection and back off from hosts that reject us. Fix return value of retry and hopelessness predicates 2019-10-01 08:34:43 +01:00
Zlatin Balevsky
66bb4eef5b close outbound establishments on a separate thread 2019-10-01 07:50:29 +01:00
Zlatin Balevsky
55f260b3f4 update version 2019-09-29 19:21:06 +01:00
88 changed files with 2418 additions and 804 deletions

View File

@@ -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. It is inspired by the LimeWire Gnutella client and developped by a former LimeWire developer.
The current stable release - 0.4.11 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 ### 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 ./gradlew clean assemble
@@ -19,13 +19,11 @@ If you want to run the unit tests, type
./gradlew clean build ./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
If you want to build binary bundles for Windows and Mac that do not depend on Java or I2P, see the https://github.com/zlatinb/muwire-pkg project
### Running ### Running
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. 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.
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` 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`

View File

@@ -23,6 +23,4 @@ To enable parsing of metadata from known file types and the user editing it or a
### Small Items ### Small Items
* Wrapper of some kind for in-place upgrades * Wrapper of some kind for in-place upgrades
* Download file sequentially
* Multiple-selection download, Ctrl-A
* Automatic adjustment of number of I2P tunnels * Automatic adjustment of number of I2P tunnels

View File

@@ -2,7 +2,7 @@ subprojects {
apply plugin: 'groovy' apply plugin: 'groovy'
dependencies { dependencies {
compile 'net.i2p:i2p:0.9.42' compile "net.i2p:i2p:${i2pVersion}"
compile 'org.codehaus.groovy:groovy-all:2.4.15' compile 'org.codehaus.groovy:groovy-all:2.4.15'
} }

View File

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

View File

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

View File

@@ -2,9 +2,9 @@ apply plugin : 'application'
mainClassName = 'com.muwire.core.Core' mainClassName = 'com.muwire.core.Core'
applicationDefaultJvmArgs = ['-Djava.util.logging.config.file=logging.properties'] applicationDefaultJvmArgs = ['-Djava.util.logging.config.file=logging.properties']
dependencies { dependencies {
compile 'net.i2p:router:0.9.42' compile "net.i2p:router:${i2pVersion}"
compile 'net.i2p.client:mstreaming:0.9.42' compile "net.i2p.client:mstreaming:${i2pVersion}"
compile 'net.i2p.client:streaming:0.9.42' compile "net.i2p.client:streaming:${i2pVersion}"
testCompile 'org.junit.jupiter:junit-jupiter-api:5.4.2' testCompile 'org.junit.jupiter:junit-jupiter-api:5.4.2'
testCompile 'junit:junit:4.12' testCompile 'junit:junit:4.12'

View File

@@ -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 = "[\\*\\+\\-,\\.:;\\(\\)=_/\\\\\\!\\\"\\\'\\\$%\\|\\[\\]\\{\\}\\?]"
}

View File

@@ -28,6 +28,8 @@ import com.muwire.core.files.FileSharedEvent
import com.muwire.core.files.FileUnsharedEvent import com.muwire.core.files.FileUnsharedEvent
import com.muwire.core.files.HasherService import com.muwire.core.files.HasherService
import com.muwire.core.files.PersisterService 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.AllFilesLoadedEvent
import com.muwire.core.files.DirectoryUnsharedEvent import com.muwire.core.files.DirectoryUnsharedEvent
import com.muwire.core.files.DirectoryWatcher import com.muwire.core.files.DirectoryWatcher
@@ -35,11 +37,13 @@ import com.muwire.core.hostcache.CacheClient
import com.muwire.core.hostcache.HostCache import com.muwire.core.hostcache.HostCache
import com.muwire.core.hostcache.HostDiscoveredEvent import com.muwire.core.hostcache.HostDiscoveredEvent
import com.muwire.core.mesh.MeshManager import com.muwire.core.mesh.MeshManager
import com.muwire.core.search.BrowseManager
import com.muwire.core.search.QueryEvent import com.muwire.core.search.QueryEvent
import com.muwire.core.search.ResultsEvent import com.muwire.core.search.ResultsEvent
import com.muwire.core.search.ResultsSender import com.muwire.core.search.ResultsSender
import com.muwire.core.search.SearchEvent import com.muwire.core.search.SearchEvent
import com.muwire.core.search.SearchManager import com.muwire.core.search.SearchManager
import com.muwire.core.search.UIBrowseEvent
import com.muwire.core.search.UIResultBatchEvent import com.muwire.core.search.UIResultBatchEvent
import com.muwire.core.trust.TrustEvent import com.muwire.core.trust.TrustEvent
import com.muwire.core.trust.TrustService import com.muwire.core.trust.TrustService
@@ -135,6 +139,7 @@ public class Core {
} else { } else {
log.info("launching embedded router") log.info("launching embedded router")
Properties routerProps = new Properties() Properties routerProps = new Properties()
routerProps.setProperty("i2p.dir.base", home.getAbsolutePath())
routerProps.setProperty("i2p.dir.config", home.getAbsolutePath()) routerProps.setProperty("i2p.dir.config", home.getAbsolutePath())
routerProps.setProperty("router.excludePeerCaps", "KLM") routerProps.setProperty("router.excludePeerCaps", "KLM")
routerProps.setProperty("i2np.inboundKBytesPerSecond", String.valueOf(props.inBw)) routerProps.setProperty("i2np.inboundKBytesPerSecond", String.valueOf(props.inBw))
@@ -214,6 +219,7 @@ public class Core {
eventBus.register(FileUnsharedEvent.class, fileManager) eventBus.register(FileUnsharedEvent.class, fileManager)
eventBus.register(SearchEvent.class, fileManager) eventBus.register(SearchEvent.class, fileManager)
eventBus.register(DirectoryUnsharedEvent.class, fileManager) eventBus.register(DirectoryUnsharedEvent.class, fileManager)
eventBus.register(UICommentEvent.class, fileManager)
log.info("initializing mesh manager") log.info("initializing mesh manager")
MeshManager meshManager = new MeshManager(fileManager, home, props) MeshManager meshManager = new MeshManager(fileManager, home, props)
@@ -222,6 +228,7 @@ public class Core {
log.info "initializing persistence service" log.info "initializing persistence service"
persisterService = new PersisterService(new File(home, "files.json"), eventBus, 60000, fileManager) persisterService = new PersisterService(new File(home, "files.json"), eventBus, 60000, fileManager)
eventBus.register(UILoadedEvent.class, persisterService) eventBus.register(UILoadedEvent.class, persisterService)
eventBus.register(UIPersistFilesEvent.class, persisterService)
log.info("initializing host cache") log.info("initializing host cache")
File hostStorage = new File(home, "hosts.json") File hostStorage = new File(home, "hosts.json")
@@ -250,7 +257,7 @@ public class Core {
I2PConnector i2pConnector = new I2PConnector(socketManager) I2PConnector i2pConnector = new I2PConnector(socketManager)
log.info "initializing results sender" 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" log.info "initializing search manager"
SearchManager searchManager = new SearchManager(eventBus, me, resultsSender) SearchManager searchManager = new SearchManager(eventBus, me, resultsSender)
@@ -276,17 +283,19 @@ public class Core {
log.info("initializing acceptor") log.info("initializing acceptor")
I2PAcceptor i2pAcceptor = new I2PAcceptor(socketManager) I2PAcceptor i2pAcceptor = new I2PAcceptor(socketManager)
connectionAcceptor = new ConnectionAcceptor(eventBus, connectionManager, props, connectionAcceptor = new ConnectionAcceptor(eventBus, connectionManager, props,
i2pAcceptor, hostCache, trustService, searchManager, uploadManager, connectionEstablisher) i2pAcceptor, hostCache, trustService, searchManager, uploadManager, fileManager, connectionEstablisher)
log.info("initializing directory watcher") log.info("initializing directory watcher")
directoryWatcher = new DirectoryWatcher(eventBus, fileManager) directoryWatcher = new DirectoryWatcher(eventBus, fileManager, home, props)
eventBus.register(FileSharedEvent.class, directoryWatcher) eventBus.register(FileSharedEvent.class, directoryWatcher)
eventBus.register(AllFilesLoadedEvent.class, directoryWatcher) eventBus.register(AllFilesLoadedEvent.class, directoryWatcher)
eventBus.register(DirectoryUnsharedEvent.class, directoryWatcher) eventBus.register(DirectoryUnsharedEvent.class, directoryWatcher)
log.info("initializing hasher service") 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(FileSharedEvent.class, hasherService)
eventBus.register(FileUnsharedEvent.class, hasherService)
eventBus.register(DirectoryUnsharedEvent.class, hasherService)
log.info("initializing trust subscriber") log.info("initializing trust subscriber")
trustSubscriber = new TrustSubscriber(eventBus, i2pConnector, props) trustSubscriber = new TrustSubscriber(eventBus, i2pConnector, props)
@@ -297,6 +306,11 @@ public class Core {
contentManager = new ContentManager() contentManager = new ContentManager()
eventBus.register(ContentControlEvent.class, contentManager) eventBus.register(ContentControlEvent.class, contentManager)
eventBus.register(QueryEvent.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() { public void startServices() {
@@ -361,7 +375,7 @@ public class Core {
} }
} }
Core core = new Core(props, home, "0.4.13") Core core = new Core(props, home, "0.5.2")
core.startServices() core.startServices()
// ... at the end, sleep or execute script // ... at the end, sleep or execute script

View File

@@ -6,11 +6,13 @@ import com.muwire.core.hostcache.CrawlerResponse
import com.muwire.core.util.DataUtil import com.muwire.core.util.DataUtil
import net.i2p.data.Base64 import net.i2p.data.Base64
import net.i2p.util.ConcurrentHashSet
class MuWireSettings { class MuWireSettings {
final boolean isLeaf final boolean isLeaf
boolean allowUntrusted boolean allowUntrusted
boolean searchExtraHop
boolean allowTrustLists boolean allowTrustLists
int trustListInterval int trustListInterval
Set<Persona> trustSubscriptions Set<Persona> trustSubscriptions
@@ -20,12 +22,17 @@ class MuWireSettings {
String updateType String updateType
String nickname String nickname
File downloadLocation File downloadLocation
File incompleteLocation
CrawlerResponse crawlerResponse CrawlerResponse crawlerResponse
boolean shareDownloadedFiles boolean shareDownloadedFiles
boolean shareHiddenFiles
boolean searchComments
boolean browseFiles
Set<String> watchedDirectories Set<String> watchedDirectories
float downloadSequentialRatio float downloadSequentialRatio
int hostClearInterval, hostHopelessInterval int hostClearInterval, hostHopelessInterval, hostRejectInterval
int meshExpiration int meshExpiration
int speedSmoothSeconds
boolean embeddedRouter boolean embeddedRouter
int inBw, outBw int inBw, outBw
Set<String> watchedKeywords Set<String> watchedKeywords
@@ -38,24 +45,33 @@ class MuWireSettings {
MuWireSettings(Properties props) { MuWireSettings(Properties props) {
isLeaf = Boolean.valueOf(props.get("leaf","false")) isLeaf = Boolean.valueOf(props.get("leaf","false"))
allowUntrusted = Boolean.valueOf(props.getProperty("allowUntrusted","true")) allowUntrusted = Boolean.valueOf(props.getProperty("allowUntrusted","true"))
searchExtraHop = Boolean.valueOf(props.getProperty("searchExtraHop","false"))
allowTrustLists = Boolean.valueOf(props.getProperty("allowTrustLists","true")) allowTrustLists = Boolean.valueOf(props.getProperty("allowTrustLists","true"))
trustListInterval = Integer.valueOf(props.getProperty("trustListInterval","1")) trustListInterval = Integer.valueOf(props.getProperty("trustListInterval","1"))
crawlerResponse = CrawlerResponse.valueOf(props.get("crawlerResponse","REGISTERED")) crawlerResponse = CrawlerResponse.valueOf(props.get("crawlerResponse","REGISTERED"))
nickname = props.getProperty("nickname","MuWireUser") nickname = props.getProperty("nickname","MuWireUser")
downloadLocation = new File((String)props.getProperty("downloadLocation", downloadLocation = new File((String)props.getProperty("downloadLocation",
System.getProperty("user.home"))) System.getProperty("user.home")))
downloadRetryInterval = Integer.parseInt(props.getProperty("downloadRetryInterval","1")) String incompleteLocationProp = props.getProperty("incompleteLocation")
if (incompleteLocationProp != null)
incompleteLocation = new File(incompleteLocationProp)
downloadRetryInterval = Integer.parseInt(props.getProperty("downloadRetryInterval","60"))
updateCheckInterval = Integer.parseInt(props.getProperty("updateCheckInterval","24")) updateCheckInterval = Integer.parseInt(props.getProperty("updateCheckInterval","24"))
autoDownloadUpdate = Boolean.parseBoolean(props.getProperty("autoDownloadUpdate","true")) autoDownloadUpdate = Boolean.parseBoolean(props.getProperty("autoDownloadUpdate","true"))
updateType = props.getProperty("updateType","jar") updateType = props.getProperty("updateType","jar")
shareDownloadedFiles = Boolean.parseBoolean(props.getProperty("shareDownloadedFiles","true")) shareDownloadedFiles = Boolean.parseBoolean(props.getProperty("shareDownloadedFiles","true"))
shareHiddenFiles = Boolean.parseBoolean(props.getProperty("shareHiddenFiles","false"))
downloadSequentialRatio = Float.valueOf(props.getProperty("downloadSequentialRatio","0.8")) downloadSequentialRatio = Float.valueOf(props.getProperty("downloadSequentialRatio","0.8"))
hostClearInterval = Integer.valueOf(props.getProperty("hostClearInterval","15")) hostClearInterval = Integer.valueOf(props.getProperty("hostClearInterval","15"))
hostHopelessInterval = Integer.valueOf(props.getProperty("hostHopelessInterval", "1440")) hostHopelessInterval = Integer.valueOf(props.getProperty("hostHopelessInterval", "1440"))
hostRejectInterval = Integer.valueOf(props.getProperty("hostRejectInterval", "1"))
meshExpiration = Integer.valueOf(props.getProperty("meshExpiration","60")) meshExpiration = Integer.valueOf(props.getProperty("meshExpiration","60"))
embeddedRouter = Boolean.valueOf(props.getProperty("embeddedRouter","false")) embeddedRouter = Boolean.valueOf(props.getProperty("embeddedRouter","false"))
inBw = Integer.valueOf(props.getProperty("inBw","256")) inBw = Integer.valueOf(props.getProperty("inBw","256"))
outBw = Integer.valueOf(props.getProperty("outBw","128")) outBw = Integer.valueOf(props.getProperty("outBw","128"))
searchComments = Boolean.valueOf(props.getProperty("searchComments","true"))
browseFiles = Boolean.valueOf(props.getProperty("browseFiles","true"))
speedSmoothSeconds = Integer.valueOf(props.getProperty("speedSmoothSeconds","60"))
watchedDirectories = readEncodedSet(props, "watchedDirectories") watchedDirectories = readEncodedSet(props, "watchedDirectories")
watchedKeywords = readEncodedSet(props, "watchedKeywords") watchedKeywords = readEncodedSet(props, "watchedKeywords")
@@ -75,23 +91,31 @@ class MuWireSettings {
Properties props = new Properties() Properties props = new Properties()
props.setProperty("leaf", isLeaf.toString()) props.setProperty("leaf", isLeaf.toString())
props.setProperty("allowUntrusted", allowUntrusted.toString()) props.setProperty("allowUntrusted", allowUntrusted.toString())
props.setProperty("searchExtraHop", String.valueOf(searchExtraHop))
props.setProperty("allowTrustLists", String.valueOf(allowTrustLists)) props.setProperty("allowTrustLists", String.valueOf(allowTrustLists))
props.setProperty("trustListInterval", String.valueOf(trustListInterval)) props.setProperty("trustListInterval", String.valueOf(trustListInterval))
props.setProperty("crawlerResponse", crawlerResponse.toString()) props.setProperty("crawlerResponse", crawlerResponse.toString())
props.setProperty("nickname", nickname) props.setProperty("nickname", nickname)
props.setProperty("downloadLocation", downloadLocation.getAbsolutePath()) props.setProperty("downloadLocation", downloadLocation.getAbsolutePath())
if (incompleteLocation != null)
props.setProperty("incompleteLocation", incompleteLocation.getAbsolutePath())
props.setProperty("downloadRetryInterval", String.valueOf(downloadRetryInterval)) props.setProperty("downloadRetryInterval", String.valueOf(downloadRetryInterval))
props.setProperty("updateCheckInterval", String.valueOf(updateCheckInterval)) props.setProperty("updateCheckInterval", String.valueOf(updateCheckInterval))
props.setProperty("autoDownloadUpdate", String.valueOf(autoDownloadUpdate)) props.setProperty("autoDownloadUpdate", String.valueOf(autoDownloadUpdate))
props.setProperty("updateType",String.valueOf(updateType)) props.setProperty("updateType",String.valueOf(updateType))
props.setProperty("shareDownloadedFiles", String.valueOf(shareDownloadedFiles)) props.setProperty("shareDownloadedFiles", String.valueOf(shareDownloadedFiles))
props.setProperty("shareHiddenFiles", String.valueOf(shareHiddenFiles))
props.setProperty("downloadSequentialRatio", String.valueOf(downloadSequentialRatio)) props.setProperty("downloadSequentialRatio", String.valueOf(downloadSequentialRatio))
props.setProperty("hostClearInterval", String.valueOf(hostClearInterval)) props.setProperty("hostClearInterval", String.valueOf(hostClearInterval))
props.setProperty("hostHopelessInterval", String.valueOf(hostHopelessInterval)) props.setProperty("hostHopelessInterval", String.valueOf(hostHopelessInterval))
props.setProperty("hostRejectInterval", String.valueOf(hostRejectInterval))
props.setProperty("meshExpiration", String.valueOf(meshExpiration)) props.setProperty("meshExpiration", String.valueOf(meshExpiration))
props.setProperty("embeddedRouter", String.valueOf(embeddedRouter)) props.setProperty("embeddedRouter", String.valueOf(embeddedRouter))
props.setProperty("inBw", String.valueOf(inBw)) props.setProperty("inBw", String.valueOf(inBw))
props.setProperty("outBw", String.valueOf(outBw)) props.setProperty("outBw", String.valueOf(outBw))
props.setProperty("searchComments", String.valueOf(searchComments))
props.setProperty("browseFiles", String.valueOf(browseFiles))
props.setProperty("speedSmoothSeconds", String.valueOf(speedSmoothSeconds))
writeEncodedSet(watchedDirectories, "watchedDirectories", props) writeEncodedSet(watchedDirectories, "watchedDirectories", props)
writeEncodedSet(watchedKeywords, "watchedKeywords", props) writeEncodedSet(watchedKeywords, "watchedKeywords", props)
@@ -108,7 +132,7 @@ class MuWireSettings {
} }
private static Set<String> readEncodedSet(Properties props, String property) { private static Set<String> readEncodedSet(Properties props, String property) {
Set<String> rv = new HashSet<>() Set<String> rv = new ConcurrentHashSet<>()
if (props.containsKey(property)) { if (props.containsKey(property)) {
String[] encoded = props.getProperty(property).split(",") String[] encoded = props.getProperty(property).split(",")
encoded.each { rv << DataUtil.readi18nString(Base64.decode(it)) } encoded.each { rv << DataUtil.readi18nString(Base64.decode(it)) }

View File

@@ -0,0 +1,7 @@
package com.muwire.core
class SplitPattern {
public static final String SPLIT_PATTERN = "[\\*\\+\\-,\\.:;\\(\\)=_/\\\\\\!\\\"\\\'\\\$%\\|\\[\\]\\{\\}\\?]";
}

View File

@@ -132,6 +132,8 @@ abstract class Connection implements Closeable {
query.firstHop = e.firstHop query.firstHop = e.firstHop
query.keywords = e.searchEvent.getSearchTerms() query.keywords = e.searchEvent.getSearchTerms()
query.oobInfohash = e.searchEvent.oobInfohash query.oobInfohash = e.searchEvent.oobInfohash
query.searchComments = e.searchEvent.searchComments
query.compressedResults = e.searchEvent.compressedResults
if (e.searchEvent.searchHash != null) if (e.searchEvent.searchHash != null)
query.infohash = Base64.encode(e.searchEvent.searchHash) query.infohash = Base64.encode(e.searchEvent.searchHash)
query.replyTo = e.replyTo.toBase64() query.replyTo = e.replyTo.toBase64()
@@ -209,11 +211,19 @@ abstract class Connection implements Closeable {
boolean oob = false boolean oob = false
if (search.oobInfohash != null) if (search.oobInfohash != null)
oob = search.oobInfohash oob = search.oobInfohash
boolean searchComments = false
if (search.searchComments != null)
searchComments = search.searchComments
boolean compressedResults = false
if (search.compressedResults != null)
compressedResults = search.compressedResults
SearchEvent searchEvent = new SearchEvent(searchTerms : search.keywords, SearchEvent searchEvent = new SearchEvent(searchTerms : search.keywords,
searchHash : infohash, searchHash : infohash,
uuid : uuid, uuid : uuid,
oobInfohash : oob) oobInfohash : oob,
searchComments : searchComments,
compressedResults : compressedResults)
QueryEvent event = new QueryEvent ( searchEvent : searchEvent, QueryEvent event = new QueryEvent ( searchEvent : searchEvent,
replyTo : replyTo, replyTo : replyTo,
originator : originator, originator : originator,

View File

@@ -5,11 +5,15 @@ import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors import java.util.concurrent.Executors
import java.util.logging.Level import java.util.logging.Level
import java.util.zip.DeflaterOutputStream import java.util.zip.DeflaterOutputStream
import java.util.zip.GZIPInputStream
import java.util.zip.GZIPOutputStream
import java.util.zip.InflaterInputStream import java.util.zip.InflaterInputStream
import com.muwire.core.Constants
import com.muwire.core.EventBus import com.muwire.core.EventBus
import com.muwire.core.MuWireSettings import com.muwire.core.MuWireSettings
import com.muwire.core.Persona import com.muwire.core.Persona
import com.muwire.core.files.FileManager
import com.muwire.core.hostcache.HostCache import com.muwire.core.hostcache.HostCache
import com.muwire.core.trust.TrustLevel import com.muwire.core.trust.TrustLevel
import com.muwire.core.trust.TrustService import com.muwire.core.trust.TrustService
@@ -17,6 +21,7 @@ import com.muwire.core.upload.UploadManager
import com.muwire.core.util.DataUtil import com.muwire.core.util.DataUtil
import com.muwire.core.search.InvalidSearchResultException import com.muwire.core.search.InvalidSearchResultException
import com.muwire.core.search.ResultsParser import com.muwire.core.search.ResultsParser
import com.muwire.core.search.ResultsSender
import com.muwire.core.search.SearchManager import com.muwire.core.search.SearchManager
import com.muwire.core.search.UIResultBatchEvent import com.muwire.core.search.UIResultBatchEvent
import com.muwire.core.search.UIResultEvent import com.muwire.core.search.UIResultEvent
@@ -25,6 +30,7 @@ import com.muwire.core.search.UnexpectedResultsException
import groovy.json.JsonOutput import groovy.json.JsonOutput
import groovy.json.JsonSlurper import groovy.json.JsonSlurper
import groovy.util.logging.Log import groovy.util.logging.Log
import net.i2p.data.Base64
@Log @Log
class ConnectionAcceptor { class ConnectionAcceptor {
@@ -37,6 +43,7 @@ class ConnectionAcceptor {
final TrustService trustService final TrustService trustService
final SearchManager searchManager final SearchManager searchManager
final UploadManager uploadManager final UploadManager uploadManager
final FileManager fileManager
final ConnectionEstablisher establisher final ConnectionEstablisher establisher
final ExecutorService acceptorThread final ExecutorService acceptorThread
@@ -47,7 +54,7 @@ class ConnectionAcceptor {
ConnectionAcceptor(EventBus eventBus, UltrapeerConnectionManager manager, ConnectionAcceptor(EventBus eventBus, UltrapeerConnectionManager manager,
MuWireSettings settings, I2PAcceptor acceptor, HostCache hostCache, MuWireSettings settings, I2PAcceptor acceptor, HostCache hostCache,
TrustService trustService, SearchManager searchManager, UploadManager uploadManager, TrustService trustService, SearchManager searchManager, UploadManager uploadManager,
ConnectionEstablisher establisher) { FileManager fileManager, ConnectionEstablisher establisher) {
this.eventBus = eventBus this.eventBus = eventBus
this.manager = manager this.manager = manager
this.settings = settings this.settings = settings
@@ -55,6 +62,7 @@ class ConnectionAcceptor {
this.hostCache = hostCache this.hostCache = hostCache
this.trustService = trustService this.trustService = trustService
this.searchManager = searchManager this.searchManager = searchManager
this.fileManager = fileManager
this.uploadManager = uploadManager this.uploadManager = uploadManager
this.establisher = establisher this.establisher = establisher
@@ -126,9 +134,15 @@ class ConnectionAcceptor {
case (byte)'P': case (byte)'P':
processPOST(e) processPOST(e)
break break
case (byte)'R':
processRESULTS(e)
break
case (byte)'T': case (byte)'T':
processTRUST(e) processTRUST(e)
break break
case (byte)'B':
processBROWSE(e)
break
default: default:
throw new Exception("Invalid read $read") throw new Exception("Invalid read $read")
} }
@@ -229,7 +243,7 @@ class ConnectionAcceptor {
Persona sender = new Persona(dis) Persona sender = new Persona(dis)
if (sender.destination != e.getDestination()) if (sender.destination != e.getDestination())
throw new IOException("Sender destination mismatch expected $e.getDestination(), got $sender.destination") throw new IOException("Sender destination mismatch expected ${e.getDestination()}, got $sender.destination")
int nResults = dis.readUnsignedShort() int nResults = dis.readUnsignedShort()
UIResultEvent[] results = new UIResultEvent[nResults] UIResultEvent[] results = new UIResultEvent[nResults]
for (int i = 0; i < nResults; i++) { for (int i = 0; i < nResults; i++) {
@@ -246,44 +260,147 @@ class ConnectionAcceptor {
e.close() e.close()
} }
} }
private void processRESULTS(Endpoint e) {
InputStream is = e.getInputStream()
DataInputStream dis = new DataInputStream(is)
byte[] esults = new byte[7]
dis.readFully(esults)
if (esults != "ESULTS ".getBytes(StandardCharsets.US_ASCII))
throw new IOException("Invalid RESULTS connection")
JsonSlurper slurper = new JsonSlurper()
try {
String uuid = DataUtil.readTillRN(dis)
UUID resultsUUID = UUID.fromString(uuid)
if (!searchManager.hasLocalSearch(resultsUUID))
throw new UnexpectedResultsException(resultsUUID.toString())
// 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("Sender"))
throw new IOException("No Sender header")
if (!headers.containsKey("Count"))
throw new IOException("No Count header")
byte [] personaBytes = Base64.decode(headers['Sender'])
Persona sender = new Persona(new ByteArrayInputStream(personaBytes))
if (sender.destination != e.getDestination())
throw new IOException("Sender destination mismatch expected ${e.getDestination()}, got $sender.destination")
int nResults = Integer.parseInt(headers['Count'])
if (nResults > Constants.MAX_RESULTS)
throw new IOException("too many results $nResults")
dis = new DataInputStream(new GZIPInputStream(dis))
UIResultEvent[] results = new UIResultEvent[nResults]
for (int i = 0; i < nResults; i++) {
int jsonSize = dis.readUnsignedShort()
byte [] payload = new byte[jsonSize]
dis.readFully(payload)
def json = slurper.parse(payload)
results[i] = ResultsParser.parse(sender, resultsUUID, json)
}
eventBus.publish(new UIResultBatchEvent(uuid: resultsUUID, results: results))
} catch (IOException bad) {
log.log(Level.WARNING, "failed to process RESULTS", bad)
} finally {
e.close()
}
}
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(new GZIPOutputStream(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()
dos.close()
} finally {
e.close()
}
}
private void processTRUST(Endpoint e) { private void processTRUST(Endpoint e) {
byte[] RUST = new byte[6] try {
DataInputStream dis = new DataInputStream(e.getInputStream()) byte[] RUST = new byte[6]
dis.readFully(RUST) DataInputStream dis = new DataInputStream(e.getInputStream())
if (RUST != "RUST\r\n".getBytes(StandardCharsets.US_ASCII)) dis.readFully(RUST)
throw new IOException("Invalid TRUST connection") if (RUST != "RUST\r\n".getBytes(StandardCharsets.US_ASCII))
String header throw new IOException("Invalid TRUST connection")
while ((header = DataUtil.readTillRN(dis)) != ""); // ignore headers for now String header
while ((header = DataUtil.readTillRN(dis)) != ""); // ignore headers for now
OutputStream os = e.getOutputStream() OutputStream os = e.getOutputStream()
if (!settings.allowTrustLists) { if (!settings.allowTrustLists) {
os.write("403 Not Allowed\r\n\r\n".getBytes(StandardCharsets.US_ASCII)) os.write("403 Not Allowed\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
os.flush() 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() 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()
e.close()
} }
} }

View File

@@ -31,7 +31,7 @@ class ConnectionEstablisher {
final HostCache hostCache final HostCache hostCache
final Timer timer final Timer timer
final ExecutorService executor final ExecutorService executor, closer
final Set inProgress = new ConcurrentHashSet() final Set inProgress = new ConcurrentHashSet()
@@ -51,6 +51,8 @@ class ConnectionEstablisher {
rv.setName("connector-${System.currentTimeMillis()}") rv.setName("connector-${System.currentTimeMillis()}")
rv rv
} as ThreadFactory) } as ThreadFactory)
closer = Executors.newSingleThreadExecutor()
} }
void start() { void start() {
@@ -60,6 +62,7 @@ class ConnectionEstablisher {
void stop() { void stop() {
timer.cancel() timer.cancel()
executor.shutdownNow() executor.shutdownNow()
closer.shutdown()
} }
private void connectIfNeeded() { private void connectIfNeeded() {
@@ -120,8 +123,10 @@ class ConnectionEstablisher {
} }
private void fail(Endpoint endpoint) { private void fail(Endpoint endpoint) {
endpoint.close() closer.execute {
eventBus.publish(new ConnectionEvent(endpoint: endpoint, incoming: false, leaf: false, status: ConnectionAttemptStatus.FAILED)) endpoint.close()
eventBus.publish(new ConnectionEvent(endpoint: endpoint, incoming: false, leaf: false, status: ConnectionAttemptStatus.FAILED))
} as Runnable
} }
private void readK(Endpoint e) { private void readK(Endpoint e) {
@@ -175,7 +180,7 @@ class ConnectionEstablisher {
log.log(Level.WARNING,"Problem parsing post-rejection payload",ignore) log.log(Level.WARNING,"Problem parsing post-rejection payload",ignore)
} finally { } finally {
// the end // the end
e.close() closer.execute({e.close()} as Runnable)
} }
} }

View File

@@ -34,7 +34,7 @@ public class DownloadManager {
private final MuWireSettings muSettings private final MuWireSettings muSettings
private final I2PConnector connector private final I2PConnector connector
private final Executor executor private final Executor executor
private final File incompletes, home private final File home
private final Persona me private final Persona me
private final Map<InfoHash, Downloader> downloaders = new ConcurrentHashMap<>() private final Map<InfoHash, Downloader> downloaders = new ConcurrentHashMap<>()
@@ -46,12 +46,9 @@ public class DownloadManager {
this.meshManager = meshManager this.meshManager = meshManager
this.muSettings = muSettings this.muSettings = muSettings
this.connector = connector this.connector = connector
this.incompletes = new File(home,"incompletes")
this.home = home this.home = home
this.me = me this.me = me
incompletes.mkdir()
this.executor = Executors.newCachedThreadPool({ r -> this.executor = Executors.newCachedThreadPool({ r ->
Thread rv = new Thread(r) Thread rv = new Thread(r)
rv.setName("download-worker") rv.setName("download-worker")
@@ -63,6 +60,11 @@ public class DownloadManager {
public void onUIDownloadEvent(UIDownloadEvent e) { public void onUIDownloadEvent(UIDownloadEvent e) {
File incompletes = muSettings.incompleteLocation
if (incompletes == null)
incompletes = new File(home, "incompletes")
incompletes.mkdirs()
def size = e.result[0].size def size = e.result[0].size
def infohash = e.result[0].infohash def infohash = e.result[0].infohash
def pieceSize = e.result[0].pieceSize def pieceSize = e.result[0].pieceSize
@@ -74,7 +76,7 @@ public class DownloadManager {
destinations.addAll(e.sources) destinations.addAll(e.sources)
destinations.remove(me.destination) 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, def downloader = new Downloader(eventBus, this, me, e.target, size,
infohash, pieceSize, connector, destinations, infohash, pieceSize, connector, destinations,
@@ -122,8 +124,18 @@ public class DownloadManager {
byte [] root = Base64.decode(json.hashRoot) byte [] root = Base64.decode(json.hashRoot)
infoHash = new InfoHash(root) infoHash = new InfoHash(root)
} }
boolean sequential = false
if (json.sequential != null)
sequential = json.sequential
File incompletes
if (json.incompletes != null)
incompletes = new File(DataUtil.readi18nString(Base64.decode(json.incompletes)))
else
incompletes = new File(home, "incompletes")
Pieces pieces = getPieces(infoHash, (long)json.length, json.pieceSizePow2) Pieces pieces = getPieces(infoHash, (long)json.length, json.pieceSizePow2, sequential)
def downloader = new Downloader(eventBus, this, me, file, (long)json.length, def downloader = new Downloader(eventBus, this, me, file, (long)json.length,
infoHash, json.pieceSizePow2, connector, destinations, incompletes, pieces) infoHash, json.pieceSizePow2, connector, destinations, incompletes, pieces)
@@ -137,12 +149,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 pieceSize = 0x1 << pieceSizePow2
int nPieces = (int)(length / pieceSize) int nPieces = (int)(length / pieceSize)
if (length % pieceSize != 0) if (length % pieceSize != 0)
nPieces++ nPieces++
Mesh mesh = meshManager.getOrCreate(infoHash, nPieces) Mesh mesh = meshManager.getOrCreate(infoHash, nPieces, sequential)
mesh.pieces mesh.pieces
} }
@@ -188,6 +200,11 @@ public class DownloadManager {
json.hashRoot = Base64.encode(infoHash.getRoot()) json.hashRoot = Base64.encode(infoHash.getRoot())
json.paused = downloader.paused json.paused = downloader.paused
json.sequential = downloader.pieces.ratio == 0f
json.incompletes = Base64.encode(DataUtil.encodei18nString(downloader.incompletes.getAbsolutePath()))
writer.println(JsonOutput.toJson(json)) writer.println(JsonOutput.toJson(json))
} }
} }

View File

@@ -27,6 +27,7 @@ import net.i2p.util.ConcurrentHashSet
@Log @Log
public class Downloader { public class Downloader {
public enum DownloadState { CONNECTING, HASHLIST, DOWNLOADING, FAILED, CANCELLED, PAUSED, FINISHED } public enum DownloadState { CONNECTING, HASHLIST, DOWNLOADING, FAILED, CANCELLED, PAUSED, FINISHED }
private enum WorkerState { CONNECTING, HASHLIST, DOWNLOADING, FINISHED} private enum WorkerState { CONNECTING, HASHLIST, DOWNLOADING, FINISHED}
@@ -48,6 +49,7 @@ public class Downloader {
private final I2PConnector connector private final I2PConnector connector
private final Set<Destination> destinations private final Set<Destination> destinations
private final int nPieces private final int nPieces
private final File incompletes
private final File piecesFile private final File piecesFile
private final File incompleteFile private final File incompleteFile
final int pieceSizePow2 final int pieceSizePow2
@@ -76,16 +78,13 @@ public class Downloader {
this.length = length this.length = length
this.connector = connector this.connector = connector
this.destinations = destinations this.destinations = destinations
this.incompletes = incompletes
this.piecesFile = new File(incompletes, file.getName()+".pieces") this.piecesFile = new File(incompletes, file.getName()+".pieces")
this.incompleteFile = new File(incompletes, file.getName()+".part") this.incompleteFile = new File(incompletes, file.getName()+".part")
this.pieceSizePow2 = pieceSizePow2 this.pieceSizePow2 = pieceSizePow2
this.pieceSize = 1 << pieceSizePow2 this.pieceSize = 1 << pieceSizePow2
this.pieces = pieces this.pieces = pieces
this.nPieces = pieces.nPieces 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() { public synchronized InfoHash getInfoHash() {
@@ -145,6 +144,12 @@ public class Downloader {
currSpeed += it.speed() currSpeed += it.speed()
} }
} }
if (speedArr.size() != downloadManager.muSettings.speedSmoothSeconds) {
speedArr.clear()
downloadManager.muSettings.speedSmoothSeconds.times { speedArr.add(0) }
speedPos = 0
}
// normalize to speedArr.size // normalize to speedArr.size
currSpeed /= speedArr.size() currSpeed /= speedArr.size()

View File

@@ -17,7 +17,7 @@ class Pieces {
done = new BitSet(nPieces) done = new BitSet(nPieces)
claimed = new BitSet(nPieces) claimed = new BitSet(nPieces)
} }
synchronized int[] claim() { synchronized int[] claim() {
int claimedCardinality = claimed.cardinality() int claimedCardinality = claimed.cardinality()
if (claimedCardinality == nPieces) { if (claimedCardinality == nPieces) {
@@ -30,7 +30,7 @@ class Pieces {
} }
// if fuller than ratio just do sequential // if fuller than ratio just do sequential
if ( (1.0f * claimedCardinality) / nPieces > ratio) { if ( (1.0f * claimedCardinality) / nPieces >= ratio) {
int rv = claimed.nextClearBit(0) int rv = claimed.nextClearBit(0)
claimed.set(rv) claimed.set(rv)
return [rv, partials.getOrDefault(rv, 0), 0] return [rv, partials.getOrDefault(rv, 0), 0]
@@ -59,7 +59,8 @@ class Pieces {
return [rv, partials.getOrDefault(rv, 0), 1] return [rv, partials.getOrDefault(rv, 0), 1]
} }
List<Integer> toList = availableCopy.toList() List<Integer> toList = availableCopy.toList()
Collections.shuffle(toList) if (ratio > 0f)
Collections.shuffle(toList)
int rv = toList[0] int rv = toList[0]
claimed.set(rv) claimed.set(rv)
[rv, partials.getOrDefault(rv, 0), 0] [rv, partials.getOrDefault(rv, 0), 0]

View File

@@ -10,4 +10,5 @@ class UIDownloadEvent extends Event {
UIResultEvent[] result UIResultEvent[] result
Set<Destination> sources Set<Destination> sources
File target File target
boolean sequential
} }

View File

@@ -13,6 +13,7 @@ import java.nio.file.WatchService
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import com.muwire.core.EventBus import com.muwire.core.EventBus
import com.muwire.core.MuWireSettings
import com.muwire.core.SharedFile import com.muwire.core.SharedFile
import groovy.util.logging.Log import groovy.util.logging.Log
@@ -31,6 +32,8 @@ class DirectoryWatcher {
kinds = [ENTRY_CREATE, ENTRY_MODIFY, ENTRY_DELETE] kinds = [ENTRY_CREATE, ENTRY_MODIFY, ENTRY_DELETE]
} }
private final File home
private final MuWireSettings muOptions
private final EventBus eventBus private final EventBus eventBus
private final FileManager fileManager private final FileManager fileManager
private final Thread watcherThread, publisherThread private final Thread watcherThread, publisherThread
@@ -39,7 +42,9 @@ class DirectoryWatcher {
private WatchService watchService private WatchService watchService
private volatile boolean shutdown 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.eventBus = eventBus
this.fileManager = fileManager this.fileManager = fileManager
this.watcherThread = new Thread({watch() } as Runnable, "directory-watcher") this.watcherThread = new Thread({watch() } as Runnable, "directory-watcher")
@@ -64,15 +69,28 @@ class DirectoryWatcher {
void onFileSharedEvent(FileSharedEvent e) { void onFileSharedEvent(FileSharedEvent e) {
if (!e.file.isDirectory()) if (!e.file.isDirectory())
return return
Path path = e.file.getCanonicalFile().toPath() File canonical = e.file.getCanonicalFile()
Path path = canonical.toPath()
WatchKey wk = path.register(watchService, kinds) 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) { void onDirectoryUnsharedEvent(DirectoryUnsharedEvent e) {
WatchKey wk = watchedDirectories.remove(e.directory) WatchKey wk = watchedDirectories.remove(e.directory)
wk?.cancel() 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() { private void watch() {

View File

@@ -8,8 +8,10 @@ import com.muwire.core.UILoadedEvent
import com.muwire.core.search.ResultsEvent import com.muwire.core.search.ResultsEvent
import com.muwire.core.search.SearchEvent import com.muwire.core.search.SearchEvent
import com.muwire.core.search.SearchIndex import com.muwire.core.search.SearchIndex
import com.muwire.core.util.DataUtil
import groovy.util.logging.Log import groovy.util.logging.Log
import net.i2p.data.Base64
@Log @Log
class FileManager { class FileManager {
@@ -20,6 +22,7 @@ class FileManager {
final Map<InfoHash, Set<SharedFile>> rootToFiles = Collections.synchronizedMap(new HashMap<>()) final Map<InfoHash, Set<SharedFile>> rootToFiles = Collections.synchronizedMap(new HashMap<>())
final Map<File, SharedFile> fileToSharedFile = 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>> nameToFiles = new HashMap<>()
final Map<String, Set<File>> commentToFile = new HashMap<>()
final SearchIndex index = new SearchIndex() final SearchIndex index = new SearchIndex()
FileManager(EventBus eventBus, MuWireSettings settings) { FileManager(EventBus eventBus, MuWireSettings settings) {
@@ -62,6 +65,18 @@ class FileManager {
} }
existingFiles.add(sf.getFile()) 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) index.add(name)
} }
@@ -86,9 +101,45 @@ class FileManager {
nameToFiles.remove(name) nameToFiles.remove(name)
} }
} }
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) 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() { Map<File, SharedFile> getSharedFiles() {
synchronized(fileToSharedFile) { synchronized(fileToSharedFile) {
@@ -112,10 +163,15 @@ class FileManager {
} else { } else {
def names = index.search e.searchTerms def names = index.search e.searchTerms
Set<File> files = new HashSet<>() 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<>() Set<SharedFile> sharedFiles = new HashSet<>()
files.each { sharedFiles.add fileToSharedFile[it] } files.each { sharedFiles.add fileToSharedFile[it] }
files = filter(sharedFiles, e.oobInfohash) files = filter(sharedFiles, e.oobInfohash)
if (!sharedFiles.isEmpty()) if (!sharedFiles.isEmpty())
re = new ResultsEvent(results: sharedFiles.asList(), uuid: e.uuid, searchEvent: e) re = new ResultsEvent(results: sharedFiles.asList(), uuid: e.uuid, searchEvent: e)

View File

@@ -4,6 +4,7 @@ import java.util.concurrent.Executor
import java.util.concurrent.Executors import java.util.concurrent.Executors
import com.muwire.core.EventBus import com.muwire.core.EventBus
import com.muwire.core.MuWireSettings
import com.muwire.core.SharedFile import com.muwire.core.SharedFile
class HasherService { class HasherService {
@@ -11,12 +12,15 @@ class HasherService {
final FileHasher hasher final FileHasher hasher
final EventBus eventBus final EventBus eventBus
final FileManager fileManager final FileManager fileManager
final Set<File> hashed = new HashSet<>()
final MuWireSettings settings
Executor executor Executor executor
HasherService(FileHasher hasher, EventBus eventBus, FileManager fileManager) { HasherService(FileHasher hasher, EventBus eventBus, FileManager fileManager, MuWireSettings settings) {
this.hasher = hasher this.hasher = hasher
this.eventBus = eventBus this.eventBus = eventBus
this.fileManager = fileManager this.fileManager = fileManager
this.settings = settings
} }
void start() { void start() {
@@ -24,13 +28,24 @@ class HasherService {
} }
void onFileSharedEvent(FileSharedEvent evt) { void onFileSharedEvent(FileSharedEvent evt) {
if (fileManager.fileToSharedFile.containsKey(evt.file.getCanonicalFile())) File canonical = evt.file.getCanonicalFile()
if (!settings.shareHiddenFiles && canonical.isHidden())
return 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) { private void process(File f) {
f = f.getCanonicalFile()
if (f.isDirectory()) { if (f.isDirectory()) {
f.listFiles().each {eventBus.publish new FileSharedEvent(file: it) } f.listFiles().each {eventBus.publish new FileSharedEvent(file: it) }
} else { } else {

View File

@@ -3,6 +3,9 @@ package com.muwire.core.files
import java.nio.file.CopyOption import java.nio.file.CopyOption
import java.nio.file.Files import java.nio.file.Files
import java.nio.file.StandardCopyOption 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.logging.Level
import java.util.stream.Collectors import java.util.stream.Collectors
@@ -28,13 +31,16 @@ class PersisterService extends Service {
final int interval final int interval
final Timer timer final Timer timer
final FileManager fileManager 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) { PersisterService(File location, EventBus listener, int interval, FileManager fileManager) {
this.location = location this.location = location
this.listener = listener this.listener = listener
this.interval = interval this.interval = interval
this.fileManager = fileManager this.fileManager = fileManager
timer = new Timer("file persister", true) timer = new Timer("file persister timer", true)
} }
void stop() { void stop() {
@@ -44,9 +50,16 @@ class PersisterService extends Service {
void onUILoadedEvent(UILoadedEvent e) { void onUILoadedEvent(UILoadedEvent e) {
timer.schedule({load()} as TimerTask, 1) timer.schedule({load()} as TimerTask, 1)
} }
void onUIPersistFilesEvent(UIPersistFilesEvent e) {
persistFiles()
}
void load() { void load() {
Thread.currentThread().setPriority(Thread.MIN_PRIORITY)
if (location.exists() && location.isFile()) { if (location.exists() && location.isFile()) {
int loaded = 0
def slurper = new JsonSlurper() def slurper = new JsonSlurper()
try { try {
location.eachLine { location.eachLine {
@@ -56,6 +69,9 @@ class PersisterService extends Service {
if (event != null) { if (event != null) {
log.fine("loaded file $event.loadedFile.file") log.fine("loaded file $event.loadedFile.file")
listener.publish event listener.publish event
loaded++
if (loaded % 10 == 0)
Thread.sleep(20)
} }
} }
} }
@@ -109,44 +125,44 @@ class PersisterService extends Service {
List sources = (List)json.sources List sources = (List)json.sources
Set<Destination> sourceSet = sources.stream().map({d -> new Destination(d.toString())}).collect Collectors.toSet() Set<Destination> sourceSet = sources.stream().map({d -> new Destination(d.toString())}).collect Collectors.toSet()
DownloadedFile df = new DownloadedFile(file, ih, pieceSize, sourceSet) DownloadedFile df = new DownloadedFile(file, ih, pieceSize, sourceSet)
df.setComment(json.comment)
return new FileLoadedEvent(loadedFile : df) return new FileLoadedEvent(loadedFile : df)
} }
SharedFile sf = new SharedFile(file, ih, pieceSize) SharedFile sf = new SharedFile(file, ih, pieceSize)
sf.setComment(json.comment)
return new FileLoadedEvent(loadedFile: sf) return new FileLoadedEvent(loadedFile: sf)
} }
private void persistFiles() { private void persistFiles() {
def sharedFiles = fileManager.getSharedFiles() persisterExecutor.submit( {
def sharedFiles = fileManager.getSharedFiles()
File tmp = File.createTempFile("muwire-files", "tmp") File tmp = File.createTempFile("muwire-files", "tmp")
tmp.deleteOnExit() tmp.deleteOnExit()
tmp.withPrintWriter { writer -> tmp.withPrintWriter { writer ->
sharedFiles.each { k, v -> sharedFiles.each { k, v ->
def json = toJson(k,v) def json = toJson(k,v)
json = JsonOutput.toJson(json) json = JsonOutput.toJson(json)
writer.println json writer.println json
}
} }
} Files.copy(tmp.toPath(), location.toPath(), StandardCopyOption.REPLACE_EXISTING)
Files.copy(tmp.toPath(), location.toPath(), StandardCopyOption.REPLACE_EXISTING) tmp.delete()
tmp.delete() } as Runnable)
} }
private def toJson(File f, SharedFile sf) { private def toJson(File f, SharedFile sf) {
def json = [:] def json = [:]
json.file = Base64.encode DataUtil.encodei18nString(f.toString()) json.file = sf.getB64EncodedFileName()
json.length = sf.getCachedLength() json.length = sf.getCachedLength()
InfoHash ih = sf.getInfoHash() InfoHash ih = sf.getInfoHash()
json.infoHash = Base64.encode ih.getRoot() json.infoHash = sf.getB64EncodedHashRoot()
json.pieceSize = sf.getPieceSize() json.pieceSize = sf.getPieceSize()
byte [] tmp = new byte [32] json.hashList = sf.getB64EncodedHashList()
json.hashList = [] json.comment = sf.getComment()
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)
}
if (sf instanceof DownloadedFile) { if (sf instanceof DownloadedFile) {
json.sources = sf.sources.stream().map( {d -> d.toBase64()}).collect(Collectors.toList()) json.sources = sf.sources.stream().map( {d -> d.toBase64()}).collect(Collectors.toList())

View File

@@ -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
}

View File

@@ -0,0 +1,6 @@
package com.muwire.core.files
import com.muwire.core.Event
class UIPersistFilesEvent extends Event {
}

View File

@@ -7,23 +7,34 @@ class Host {
private static final int MAX_FAILURES = 3 private static final int MAX_FAILURES = 3
final Destination destination final Destination destination
private final int clearInterval, hopelessInterval private final int clearInterval, hopelessInterval, rejectionInterval
int failures,successes int failures,successes
long lastAttempt long lastAttempt
long lastSuccessfulAttempt long lastSuccessfulAttempt
long lastRejection
public Host(Destination destination, int clearInterval, int hopelessInterval) { public Host(Destination destination, int clearInterval, int hopelessInterval, int rejectionInterval) {
this.destination = destination this.destination = destination
this.clearInterval = clearInterval this.clearInterval = clearInterval
this.hopelessInterval = hopelessInterval this.hopelessInterval = hopelessInterval
this.rejectionInterval = rejectionInterval
} }
synchronized void onConnect() { private void connectSuccessful() {
failures = 0 failures = 0
successes++ successes++
lastAttempt = System.currentTimeMillis() lastAttempt = System.currentTimeMillis()
}
synchronized void onConnect() {
connectSuccessful()
lastSuccessfulAttempt = lastAttempt lastSuccessfulAttempt = lastAttempt
} }
synchronized void onReject() {
connectSuccessful()
lastRejection = lastAttempt;
}
synchronized void onFailure() { synchronized void onFailure() {
failures++ failures++
@@ -43,13 +54,17 @@ class Host {
failures = 0 failures = 0
} }
synchronized void canTryAgain() { synchronized boolean canTryAgain() {
lastSuccessfulAttempt > 0 && lastSuccessfulAttempt > 0 &&
System.currentTimeMillis() - lastAttempt > (clearInterval * 60 * 1000) System.currentTimeMillis() - lastAttempt > (clearInterval * 60 * 1000)
} }
synchronized void isHopeless() { synchronized boolean isHopeless() {
isFailed() && isFailed() &&
System.currentTimeMillis() - lastSuccessfulAttempt > (hopelessInterval * 60 * 1000) System.currentTimeMillis() - lastSuccessfulAttempt > (hopelessInterval * 60 * 1000)
} }
synchronized boolean isRecentlyRejected() {
System.currentTimeMillis() - lastRejection < (rejectionInterval * 60 * 1000)
}
} }

View File

@@ -52,7 +52,7 @@ class HostCache extends Service {
hosts.get(e.destination).clearFailures() hosts.get(e.destination).clearFailures()
return return
} }
Host host = new Host(e.destination, settings.hostClearInterval, settings.hostHopelessInterval) Host host = new Host(e.destination, settings.hostClearInterval, settings.hostHopelessInterval, settings.hostRejectInterval)
if (allowHost(host)) { if (allowHost(host)) {
hosts.put(e.destination, host) hosts.put(e.destination, host)
} }
@@ -64,15 +64,17 @@ class HostCache extends Service {
Destination dest = e.endpoint.destination Destination dest = e.endpoint.destination
Host host = hosts.get(dest) Host host = hosts.get(dest)
if (host == null) { if (host == null) {
host = new Host(dest, settings.hostClearInterval, settings.hostHopelessInterval) host = new Host(dest, settings.hostClearInterval, settings.hostHopelessInterval, settings.hostRejectInterval)
hosts.put(dest, host) hosts.put(dest, host)
} }
switch(e.status) { switch(e.status) {
case ConnectionAttemptStatus.SUCCESSFUL: case ConnectionAttemptStatus.SUCCESSFUL:
case ConnectionAttemptStatus.REJECTED:
host.onConnect() host.onConnect()
break break
case ConnectionAttemptStatus.REJECTED:
host.onReject()
break
case ConnectionAttemptStatus.FAILED: case ConnectionAttemptStatus.FAILED:
host.onFailure() host.onFailure()
break break
@@ -84,7 +86,7 @@ class HostCache extends Service {
rv.retainAll {allowHost(hosts[it])} rv.retainAll {allowHost(hosts[it])}
rv.removeAll { rv.removeAll {
def h = hosts[it]; def h = hosts[it];
h.isFailed() && !h.canTryAgain() (h.isFailed() && !h.canTryAgain()) || h.isRecentlyRejected()
} }
if (rv.size() <= n) if (rv.size() <= n)
return rv return rv
@@ -103,6 +105,22 @@ class HostCache extends Service {
Collections.shuffle(rv) Collections.shuffle(rv)
rv[0..n-1] rv[0..n-1]
} }
int countFailingHosts() {
List<Destination> rv = new ArrayList<>(hosts.keySet())
rv.retainAll {
hosts[it].isFailed()
}
rv.size()
}
int countHopelessHosts() {
List<Destination> rv = new ArrayList<>(hosts.keySet())
rv.retainAll {
hosts[it].isHopeless()
}
rv.size()
}
void load() { void load() {
if (storage.exists()) { if (storage.exists()) {
@@ -110,13 +128,15 @@ class HostCache extends Service {
storage.eachLine { storage.eachLine {
def entry = slurper.parseText(it) def entry = slurper.parseText(it)
Destination dest = new Destination(entry.destination) Destination dest = new Destination(entry.destination)
Host host = new Host(dest, settings.hostClearInterval, settings.hostHopelessInterval) Host host = new Host(dest, settings.hostClearInterval, settings.hostHopelessInterval, settings.hostRejectInterval)
host.failures = Integer.valueOf(String.valueOf(entry.failures)) host.failures = Integer.valueOf(String.valueOf(entry.failures))
host.successes = Integer.valueOf(String.valueOf(entry.successes)) host.successes = Integer.valueOf(String.valueOf(entry.successes))
if (entry.lastAttempt != null) if (entry.lastAttempt != null)
host.lastAttempt = entry.lastAttempt host.lastAttempt = entry.lastAttempt
if (entry.lastSuccessfulAttempt != null) if (entry.lastSuccessfulAttempt != null)
host.lastSuccessfulAttempt = entry.lastSuccessfulAttempt host.lastSuccessfulAttempt = entry.lastSuccessfulAttempt
if (entry.lastRejection != null)
host.lastRejection = entry.lastRejection
if (allowHost(host)) if (allowHost(host))
hosts.put(dest, host) hosts.put(dest, host)
} }
@@ -151,6 +171,7 @@ class HostCache extends Service {
map.successes = host.successes map.successes = host.successes
map.lastAttempt = host.lastAttempt map.lastAttempt = host.lastAttempt
map.lastSuccessfulAttempt = host.lastSuccessfulAttempt map.lastSuccessfulAttempt = host.lastSuccessfulAttempt
map.lastRejection = host.lastRejection
def json = JsonOutput.toJson(map) def json = JsonOutput.toJson(map)
writer.println json writer.println json
} }

View File

@@ -33,11 +33,12 @@ class MeshManager {
meshes.get(infoHash) meshes.get(infoHash)
} }
Mesh getOrCreate(InfoHash infoHash, int nPieces) { Mesh getOrCreate(InfoHash infoHash, int nPieces, boolean sequential) {
synchronized(meshes) { synchronized(meshes) {
if (meshes.containsKey(infoHash)) if (meshes.containsKey(infoHash))
return meshes.get(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)) { if (fileManager.rootToFiles.containsKey(infoHash)) {
for (int i = 0; i < nPieces; i++) for (int i = 0; i < nPieces; i++)
pieces.markDownloaded(i) pieces.markDownloaded(i)

View File

@@ -0,0 +1,87 @@
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
import java.util.zip.GZIPInputStream
@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 $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, totalResults : results))
JsonSlurper slurper = new JsonSlurper()
DataInputStream dis = new DataInputStream(new GZIPInputStream(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)
}
}

View File

@@ -0,0 +1,5 @@
package com.muwire.core.search;
public enum BrowseStatus {
CONNECTING, FETCHING, FINISHED, FAILED
}

View File

@@ -0,0 +1,8 @@
package com.muwire.core.search
import com.muwire.core.Event
class BrowseStatusEvent extends Event {
BrowseStatus status
int totalResults
}

View File

@@ -90,6 +90,14 @@ class ResultsParser {
Set<Destination> sources = Collections.emptySet() Set<Destination> sources = Collections.emptySet()
if (json.sources != null) if (json.sources != null)
sources = json.sources.stream().map({new Destination(it)}).collect(Collectors.toSet()) 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 = json.browse
return new UIResultEvent( sender : p, return new UIResultEvent( sender : p,
name : name, name : name,
@@ -97,6 +105,8 @@ class ResultsParser {
infohash : new InfoHash(infoHash), infohash : new InfoHash(infoHash),
pieceSize : pieceSize, pieceSize : pieceSize,
sources : sources, sources : sources,
comment : comment,
browse : browse,
uuid: uuid) uuid: uuid)
} catch (Exception e) { } catch (Exception e) {
throw new InvalidSearchResultException("parsing search result failed",e) throw new InvalidSearchResultException("parsing search result failed",e)

View File

@@ -4,6 +4,7 @@ import com.muwire.core.SharedFile
import com.muwire.core.connection.Endpoint import com.muwire.core.connection.Endpoint
import com.muwire.core.connection.I2PConnector import com.muwire.core.connection.I2PConnector
import com.muwire.core.files.FileHasher import com.muwire.core.files.FileHasher
import com.muwire.core.util.DataUtil
import com.muwire.core.Persona import com.muwire.core.Persona
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
@@ -13,10 +14,12 @@ import java.util.concurrent.ThreadFactory
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
import java.util.logging.Level import java.util.logging.Level
import java.util.stream.Collectors import java.util.stream.Collectors
import java.util.zip.GZIPOutputStream
import com.muwire.core.DownloadedFile import com.muwire.core.DownloadedFile
import com.muwire.core.EventBus import com.muwire.core.EventBus
import com.muwire.core.InfoHash import com.muwire.core.InfoHash
import com.muwire.core.MuWireSettings
import groovy.json.JsonOutput import groovy.json.JsonOutput
import groovy.util.logging.Log import groovy.util.logging.Log
@@ -42,16 +45,19 @@ class ResultsSender {
private final I2PConnector connector private final I2PConnector connector
private final Persona me private final Persona me
private final EventBus eventBus 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.connector = connector;
this.eventBus = eventBus this.eventBus = eventBus
this.me = me this.me = me
this.settings = settings
} }
void sendResults(UUID uuid, SharedFile[] results, Destination target, boolean oobInfohash) { void sendResults(UUID uuid, SharedFile[] results, Destination target, boolean oobInfohash, boolean compressedResults) {
log.info("Sending $results.length results for uuid $uuid to ${target.toBase32()} oobInfohash : $oobInfohash") log.info("Sending $results.length results for uuid $uuid to ${target.toBase32()} oobInfohash : $oobInfohash")
if (target.equals(me.destination)) { if (target.equals(me.destination)) {
def uiResultEvents = []
results.each { results.each {
long length = it.getFile().length() long length = it.getFile().length()
int pieceSize = it.getPieceSize() int pieceSize = it.getPieceSize()
@@ -60,19 +66,25 @@ class ResultsSender {
Set<Destination> suggested = Collections.emptySet() Set<Destination> suggested = Collections.emptySet()
if (it instanceof DownloadedFile) if (it instanceof DownloadedFile)
suggested = it.sources suggested = it.sources
def comment = null
if (it.getComment() != null) {
comment = DataUtil.readi18nString(Base64.decode(it.getComment()))
}
def uiResultEvent = new UIResultEvent( sender : me, def uiResultEvent = new UIResultEvent( sender : me,
name : it.getFile().getName(), name : it.getFile().getName(),
size : length, size : length,
infohash : it.getInfoHash(), infohash : it.getInfoHash(),
pieceSize : pieceSize, pieceSize : pieceSize,
uuid : uuid, uuid : uuid,
sources : suggested sources : suggested,
comment : comment
) )
eventBus.publish(uiResultEvent) uiResultEvents << uiResultEvent
} }
eventBus.publish(new UIResultBatchEvent(uuid : uuid, results : uiResultEvents))
} else { } else {
executor.execute(new ResultSendJob(uuid : uuid, results : results, executor.execute(new ResultSendJob(uuid : uuid, results : results,
target: target, oobInfohash : oobInfohash)) target: target, oobInfohash : oobInfohash, compressedResults : compressedResults))
} }
} }
@@ -81,58 +93,79 @@ class ResultsSender {
SharedFile [] results SharedFile [] results
Destination target Destination target
boolean oobInfohash boolean oobInfohash
boolean compressedResults
@Override @Override
public void run() { public void run() {
try { try {
byte [] tmp = new byte[InfoHash.SIZE]
JsonOutput jsonOutput = new JsonOutput() JsonOutput jsonOutput = new JsonOutput()
Endpoint endpoint = null; Endpoint endpoint = null;
try { if (!compressedResults) {
endpoint = connector.connect(target) try {
DataOutputStream os = new DataOutputStream(endpoint.getOutputStream()) endpoint = connector.connect(target)
os.write("POST $uuid\r\n\r\n".getBytes(StandardCharsets.US_ASCII)) DataOutputStream os = new DataOutputStream(endpoint.getOutputStream())
me.write(os) os.write("POST $uuid\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
os.writeShort((short)results.length) me.write(os)
results.each { os.writeShort((short)results.length)
byte [] name = it.getFile().getName().getBytes(StandardCharsets.UTF_8) results.each {
def baos = new ByteArrayOutputStream() def obj = sharedFileToObj(it, settings.browseFiles)
def daos = new DataOutputStream(baos) def json = jsonOutput.toJson(obj)
daos.writeShort((short) name.length) os.writeShort((short)json.length())
daos.write(name) os.write(json.getBytes(StandardCharsets.US_ASCII))
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
} }
os.flush()
if (it instanceof DownloadedFile) } finally {
obj.sources = it.sources.stream().map({dest -> dest.toBase64()}).collect(Collectors.toSet()) endpoint?.close()
}
def json = jsonOutput.toJson(obj) } else {
os.writeShort((short)json.length()) try {
os.write(json.getBytes(StandardCharsets.US_ASCII)) endpoint = connector.connect(target)
OutputStream os = endpoint.getOutputStream()
os.write("RESULTS $uuid\r\n".getBytes(StandardCharsets.US_ASCII))
os.write("Sender: ${me.toBase64()}\r\n".getBytes(StandardCharsets.US_ASCII))
os.write("Count: $results.length\r\n".getBytes(StandardCharsets.US_ASCII))
os.write("\r\n".getBytes(StandardCharsets.US_ASCII))
DataOutputStream dos = new DataOutputStream(new GZIPOutputStream(os))
results.each {
def obj = sharedFileToObj(it, settings.browseFiles)
def json = jsonOutput.toJson(obj)
dos.writeShort((short)json.length())
dos.write(json.getBytes(StandardCharsets.US_ASCII))
}
dos.close()
} finally {
endpoint?.close()
} }
os.flush()
} finally {
endpoint?.close()
} }
} catch (Exception e) { } catch (Exception e) {
log.log(Level.WARNING, "problem sending results",e) log.log(Level.WARNING, "problem sending results",e)
} }
} }
} }
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
}
} }

View File

@@ -9,11 +9,13 @@ class SearchEvent extends Event {
byte [] searchHash byte [] searchHash
UUID uuid UUID uuid
boolean oobInfohash boolean oobInfohash
boolean searchComments
boolean compressedResults
String toString() { String toString() {
def infoHash = null def infoHash = null
if (searchHash != null) if (searchHash != null)
infoHash = new InfoHash(searchHash) infoHash = new InfoHash(searchHash)
"searchTerms: $searchTerms searchHash:$infoHash, uuid:$uuid oobInfohash:$oobInfohash" "searchTerms: $searchTerms searchHash:$infoHash, uuid:$uuid oobInfohash:$oobInfohash searchComments:$searchComments compressedResults:$compressedResults"
} }
} }

View File

@@ -1,6 +1,6 @@
package com.muwire.core.search package com.muwire.core.search
import com.muwire.core.Constants import com.muwire.core.SplitPattern
class SearchIndex { class SearchIndex {
@@ -32,7 +32,7 @@ class SearchIndex {
} }
private static String[] split(String source) { private static String[] split(String source) {
source = source.replaceAll(Constants.SPLIT_PATTERN, " ").toLowerCase() source = source.replaceAll(SplitPattern.SPLIT_PATTERN, " ").toLowerCase()
String [] split = source.split(" ") String [] split = source.split(" ")
def rv = [] def rv = []
split.each { if (it.length() > 0) rv << it } split.each { if (it.length() > 0) rv << it }

View File

@@ -44,7 +44,7 @@ public class SearchManager {
log.info("No results for search uuid $event.uuid") log.info("No results for search uuid $event.uuid")
return return
} }
resultsSender.sendResults(event.uuid, event.results, target, event.searchEvent.oobInfohash) resultsSender.sendResults(event.uuid, event.results, target, event.searchEvent.oobInfohash, event.searchEvent.compressedResults)
} }
boolean hasLocalSearch(UUID uuid) { boolean hasLocalSearch(UUID uuid) {

View File

@@ -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
}

View File

@@ -14,6 +14,8 @@ class UIResultEvent extends Event {
long size long size
InfoHash infohash InfoHash infohash
int pieceSize int pieceSize
String comment
boolean browse
@Override @Override
public String toString() { public String toString() {

View File

@@ -92,7 +92,7 @@ public class UploadManager {
pieceSize = downloader.pieceSizePow2 pieceSize = downloader.pieceSizePow2
} else { } else {
SharedFile sharedFile = sharedFiles.iterator().next(); SharedFile sharedFile = sharedFiles.iterator().next();
mesh = meshManager.getOrCreate(request.infoHash, sharedFile.NPieces) mesh = meshManager.getOrCreate(request.infoHash, sharedFile.NPieces, false)
file = sharedFile.file file = sharedFile.file
pieceSize = sharedFile.pieceSize pieceSize = sharedFile.pieceSize
} }
@@ -217,7 +217,7 @@ public class UploadManager {
pieceSize = downloader.pieceSizePow2 pieceSize = downloader.pieceSizePow2
} else { } else {
SharedFile sharedFile = sharedFiles.iterator().next(); SharedFile sharedFile = sharedFiles.iterator().next();
mesh = meshManager.getOrCreate(request.infoHash, sharedFile.NPieces) mesh = meshManager.getOrCreate(request.infoHash, sharedFile.NPieces, false)
file = sharedFile.file file = sharedFile.file
pieceSize = sharedFile.pieceSize pieceSize = sharedFile.pieceSize
} }

View File

@@ -0,0 +1,13 @@
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;
public static final int MAX_RESULTS = 0x1 << 16;
}

View File

@@ -2,6 +2,12 @@ package com.muwire.core;
import java.io.File; import java.io.File;
import java.io.IOException; 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 { public class SharedFile {
@@ -11,6 +17,12 @@ public class SharedFile {
private final String cachedPath; private final String cachedPath;
private final long cachedLength; 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 { public SharedFile(File file, InfoHash infoHash, int pieceSize) throws IOException {
this.file = file; this.file = file;
@@ -18,6 +30,16 @@ public class SharedFile {
this.pieceSize = pieceSize; this.pieceSize = pieceSize;
this.cachedPath = file.getAbsolutePath(); this.cachedPath = file.getAbsolutePath();
this.cachedLength = file.length(); 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() { public File getFile() {
@@ -40,6 +62,18 @@ public class SharedFile {
rv++; rv++;
return rv; return rv;
} }
public String getB64EncodedFileName() {
return b64EncodedFileName;
}
public String getB64EncodedHashRoot() {
return b64EncodedHashRoot;
}
public List<String> getB64EncodedHashList() {
return b64EncodedHashList;
}
public String getCachedPath() { public String getCachedPath() {
return cachedPath; return cachedPath;
@@ -48,6 +82,14 @@ public class SharedFile {
public long getCachedLength() { public long getCachedLength() {
return cachedLength; return cachedLength;
} }
public void setComment(String comment) {
this.comment = comment;
}
public String getComment() {
return comment;
}
@Override @Override
public int hashCode() { public int hashCode() {

View File

@@ -1,122 +1,134 @@
package com.muwire.core.util package com.muwire.core.util;
import java.lang.reflect.Field import java.io.ByteArrayOutputStream;
import java.lang.reflect.Method import java.io.DataOutputStream;
import java.nio.ByteBuffer import java.io.IOException;
import java.nio.charset.StandardCharsets 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 com.muwire.core.Constants;
import net.i2p.data.Base64 import net.i2p.data.Base64;
class DataUtil { public class DataUtil {
private final static int MAX_SHORT = (0x1 << 16) - 1 private final static int MAX_SHORT = (0x1 << 16) - 1;
static void writeUnsignedShort(int value, OutputStream os) { static void writeUnsignedShort(int value, OutputStream os) throws IOException {
if (value > MAX_SHORT || value < 0) if (value > MAX_SHORT || value < 0)
throw new IllegalArgumentException("$value invalid") throw new IllegalArgumentException("$value invalid");
byte lsb = (byte) (value & 0xFF) byte lsb = (byte) (value & 0xFF);
byte msb = (byte) (value >> 8) byte msb = (byte) (value >> 8);
os.write(msb) os.write(msb);
os.write(lsb) os.write(lsb);
} }
private final static int MAX_HEADER = 0x7FFFFF private final static int MAX_HEADER = 0x7FFFFF;
static void packHeader(int length, byte [] header) { static void packHeader(int length, byte [] header) {
if (header.length != 3) if (header.length != 3)
throw new IllegalArgumentException("header length $header.length") throw new IllegalArgumentException("header length $header.length");
if (length < 0 || length > MAX_HEADER) if (length < 0 || length > MAX_HEADER)
throw new IllegalArgumentException("length $length") throw new IllegalArgumentException("length $length");
header[2] = (byte) (length & 0xFF) header[2] = (byte) (length & 0xFF);
header[1] = (byte) ((length >> 8) & 0xFF) header[1] = (byte) ((length >> 8) & 0xFF);
header[0] = (byte) ((length >> 16) & 0x7F) header[0] = (byte) ((length >> 16) & 0x7F);
} }
static int readLength(byte [] header) { static int readLength(byte [] header) {
if (header.length != 3) if (header.length != 3)
throw new IllegalArgumentException("header length $header.length") throw new IllegalArgumentException("header length $header.length");
return (((int)(header[0] & 0x7F)) << 16) | return (((int)(header[0] & 0x7F)) << 16) |
(((int)(header[1] & 0xFF) << 8)) | (((int)(header[1] & 0xFF) << 8)) |
((int)header[2] & 0xFF) ((int)header[2] & 0xFF);
} }
static String readi18nString(byte [] encoded) { static String readi18nString(byte [] encoded) {
if (encoded.length < 2) if (encoded.length < 2)
throw new IllegalArgumentException("encoding too short $encoded.length") throw new IllegalArgumentException("encoding too short $encoded.length");
int length = ((encoded[0] & 0xFF) << 8) | (encoded[1] & 0xFF) int length = ((encoded[0] & 0xFF) << 8) | (encoded[1] & 0xFF);
if (encoded.length != length + 2) if (encoded.length != length + 2)
throw new IllegalArgumentException("encoding doesn't match length, expected $length found $encoded.length") throw new IllegalArgumentException("encoding doesn't match length, expected $length found $encoded.length");
byte [] string = new byte[length] byte [] string = new byte[length];
System.arraycopy(encoded, 2, string, 0, length) System.arraycopy(encoded, 2, string, 0, length);
new String(string, StandardCharsets.UTF_8) return new String(string, StandardCharsets.UTF_8);
} }
static byte[] encodei18nString(String string) { public static byte[] encodei18nString(String string) {
byte [] utf8 = string.getBytes(StandardCharsets.UTF_8) byte [] utf8 = string.getBytes(StandardCharsets.UTF_8);
if (utf8.length > Short.MAX_VALUE) if (utf8.length > Short.MAX_VALUE)
throw new IllegalArgumentException("String in utf8 too long $utf8.length") throw new IllegalArgumentException("String in utf8 too long $utf8.length");
def baos = new ByteArrayOutputStream() ByteArrayOutputStream baos = new ByteArrayOutputStream();
def daos = new DataOutputStream(baos) DataOutputStream daos = new DataOutputStream(baos);
daos.writeShort((short) utf8.length) try {
daos.write(utf8) daos.writeShort((short) utf8.length);
daos.close() daos.write(utf8);
baos.toByteArray() daos.close();
} catch (IOException impossible) {
throw new IllegalStateException(impossible);
}
return baos.toByteArray();
} }
public static String readTillRN(InputStream is) { public static String readTillRN(InputStream is) throws IOException {
def baos = new ByteArrayOutputStream() ByteArrayOutputStream baos = new ByteArrayOutputStream();
while(baos.size() < (Constants.MAX_HEADER_SIZE)) { while(baos.size() < (Constants.MAX_HEADER_SIZE)) {
byte read = is.read() int read = is.read();
if (read == -1) if (read == -1)
throw new IOException() throw new IOException();
if (read == '\r') { if (read == '\r') {
if (is.read() != '\n') if (is.read() != '\n')
throw new IOException("invalid header") throw new IOException("invalid header");
break break;
} }
baos.write(read) baos.write(read);
} }
new String(baos.toByteArray(), StandardCharsets.US_ASCII) return new String(baos.toByteArray(), StandardCharsets.US_ASCII);
} }
public static String encodeXHave(List<Integer> pieces, int totalPieces) { public static String encodeXHave(List<Integer> pieces, int totalPieces) {
int bytes = totalPieces / 8 int bytes = totalPieces / 8;
if (totalPieces % 8 != 0) if (totalPieces % 8 != 0)
bytes++ bytes++;
byte[] raw = new byte[bytes] byte[] raw = new byte[bytes];
pieces.each { for (int it : pieces) {
int byteIdx = it / 8 int byteIdx = it / 8;
int offset = it % 8 int offset = it % 8;
int mask = 0x80 >>> offset int mask = 0x80 >>> offset;
raw[byteIdx] |= mask raw[byteIdx] |= mask;
} }
Base64.encode(raw) return Base64.encode(raw);
} }
public static List<Integer> decodeXHave(String xHave) { public static List<Integer> decodeXHave(String xHave) {
byte [] availablePieces = Base64.decode(xHave) byte [] availablePieces = Base64.decode(xHave);
List<Integer> available = new ArrayList<>() List<Integer> available = new ArrayList<>();
availablePieces.eachWithIndex {b, i -> for (int i = 0; i < availablePieces.length; i ++) {
byte b = availablePieces[i];
for (int j = 0; j < 8 ; j++) { for (int j = 0; j < 8 ; j++) {
byte mask = 0x80 >>> j byte mask = (byte) (0x80 >>> j);
if ((b & mask) == mask) { if ((b & mask) == mask) {
available.add(i * 8 + j) available.add(i * 8 + j);
} }
} }
} }
available return available;
} }
public static Exception findRoot(Exception e) { public static Throwable findRoot(Throwable e) {
while(e.getCause() != null) while(e.getCause() != null)
e = e.getCause() e = e.getCause();
e return e;
} }
public static void tryUnmap(ByteBuffer cb) { public static void tryUnmap(ByteBuffer cb) {

View File

@@ -4,6 +4,7 @@ import static org.junit.Assert.fail
import org.junit.After import org.junit.After
import org.junit.Before import org.junit.Before
import org.junit.Ignore
import org.junit.Test import org.junit.Test
import com.muwire.core.EventBus import com.muwire.core.EventBus
@@ -180,10 +181,11 @@ class DownloadSessionTest {
} }
@Test @Test
@Ignore // this needs to be rewritten with stealing in mind
public void testSmallFileClaimed() { public void testSmallFileClaimed() {
initSession(20, [0]) initSession(20, [0])
long now = System.currentTimeMillis() long now = System.currentTimeMillis()
downloadThread.join(100) downloadThread.join(150)
assert 100 >= (System.currentTimeMillis() - now) assert 100 >= (System.currentTimeMillis() - now)
assert !performed assert !performed
assert available.isEmpty() assert available.isEmpty()

View File

@@ -16,7 +16,7 @@ class PiecesTest {
public void testSinglePiece() { public void testSinglePiece() {
pieces = new Pieces(1) pieces = new Pieces(1)
assert !pieces.isComplete() assert !pieces.isComplete()
assert pieces.claim() == 0 assert pieces.claim() == [0,0,0]
pieces.markDownloaded(0) pieces.markDownloaded(0)
assert pieces.isComplete() assert pieces.isComplete()
} }
@@ -25,28 +25,28 @@ class PiecesTest {
public void testTwoPieces() { public void testTwoPieces() {
pieces = new Pieces(2) pieces = new Pieces(2)
assert !pieces.isComplete() assert !pieces.isComplete()
int piece = pieces.claim() int[] piece = pieces.claim()
assert piece == 0 || piece == 1 assert piece[0] == 0 || piece[0] == 1
pieces.markDownloaded(piece) pieces.markDownloaded(piece[0])
assert !pieces.isComplete() assert !pieces.isComplete()
int piece2 = pieces.claim() int[] piece2 = pieces.claim()
assert piece != piece2 assert piece[0] != piece2[0]
pieces.markDownloaded(piece2) pieces.markDownloaded(piece2[0])
assert pieces.isComplete() assert pieces.isComplete()
} }
@Test @Test
public void testClaimAvailable() { public void testClaimAvailable() {
pieces = new Pieces(2) pieces = new Pieces(2)
int claimed = pieces.claim([0].toSet()) int[] claimed = pieces.claim([0].toSet())
assert claimed == 0 assert claimed == [0,0,0]
assert -1 == pieces.claim([0].toSet()) assert [0,0,1] == pieces.claim([0].toSet())
} }
@Test @Test
public void testClaimNoneAvailable() { public void testClaimNoneAvailable() {
pieces = new Pieces(20) pieces = new Pieces(20)
int claimed = pieces.claim() int[] claimed = pieces.claim()
assert -1 == pieces.claim([claimed].toSet()) assert [0,0,0] == pieces.claim(claimed.toSet())
} }
} }

View File

@@ -25,7 +25,8 @@ class HasherServiceTest {
void before() { void before() {
eventBus = new EventBus() eventBus = new EventBus()
hasher = new FileHasher() 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(FileHashedEvent.class, listener)
eventBus.register(FileSharedEvent.class, service) eventBus.register(FileSharedEvent.class, service)
service.start() service.start()

View File

@@ -72,6 +72,9 @@ class HostCacheTest {
TrustLevel.NEUTRAL TrustLevel.NEUTRAL
} }
settingsMock.ignore.allowUntrusted { true } settingsMock.ignore.allowUntrusted { true }
settingsMock.ignore.getHostClearInterval { 0 }
settingsMock.ignore.getHostHopelessInterval { 0 }
settingsMock.ignore.getHostRejectInterval { 0 }
initMocks() initMocks()
@@ -91,6 +94,10 @@ class HostCacheTest {
TrustLevel.DISTRUSTED TrustLevel.DISTRUSTED
} }
settingsMock.ignore.getHostClearInterval { 0 }
settingsMock.ignore.getHostHopelessInterval { 0 }
settingsMock.ignore.getHostRejectInterval { 0 }
initMocks() initMocks()
cache.onHostDiscoveredEvent(new HostDiscoveredEvent(destination: destinations.dest1)) cache.onHostDiscoveredEvent(new HostDiscoveredEvent(destination: destinations.dest1))
@@ -104,6 +111,9 @@ class HostCacheTest {
TrustLevel.NEUTRAL TrustLevel.NEUTRAL
} }
settingsMock.ignore.allowUntrusted { false } settingsMock.ignore.allowUntrusted { false }
settingsMock.ignore.getHostClearInterval { 0 }
settingsMock.ignore.getHostHopelessInterval { 0 }
settingsMock.ignore.getHostRejectInterval { 0 }
initMocks() initMocks()
@@ -123,6 +133,9 @@ class HostCacheTest {
} }
trustMock.demand.getLevel{ d -> TrustLevel.TRUSTED } trustMock.demand.getLevel{ d -> TrustLevel.TRUSTED }
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() initMocks()
cache.onHostDiscoveredEvent(new HostDiscoveredEvent(destination: destinations.dest1)) cache.onHostDiscoveredEvent(new HostDiscoveredEvent(destination: destinations.dest1))
@@ -139,7 +152,15 @@ class HostCacheTest {
assert d == destinations.dest1 assert d == destinations.dest1
TrustLevel.TRUSTED 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() initMocks()
cache.onHostDiscoveredEvent(new HostDiscoveredEvent(destination: destinations.dest1)) cache.onHostDiscoveredEvent(new HostDiscoveredEvent(destination: destinations.dest1))
@@ -158,6 +179,10 @@ class HostCacheTest {
TrustLevel.TRUSTED TrustLevel.TRUSTED
} }
settingsMock.ignore.getHostClearInterval { 0 }
settingsMock.ignore.getHostHopelessInterval { 0 }
settingsMock.ignore.getHostRejectInterval { 0 }
initMocks() initMocks()
cache.onHostDiscoveredEvent(new HostDiscoveredEvent(destination: destinations.dest1)) cache.onHostDiscoveredEvent(new HostDiscoveredEvent(destination: destinations.dest1))
@@ -183,6 +208,10 @@ class HostCacheTest {
TrustLevel.TRUSTED TrustLevel.TRUSTED
} }
settingsMock.ignore.getHostClearInterval { 0 }
settingsMock.ignore.getHostHopelessInterval { 0 }
settingsMock.ignore.getHostRejectInterval { 0 }
initMocks() initMocks()
cache.onHostDiscoveredEvent(new HostDiscoveredEvent(destination: destinations.dest1)) cache.onHostDiscoveredEvent(new HostDiscoveredEvent(destination: destinations.dest1))
@@ -214,6 +243,10 @@ class HostCacheTest {
TrustLevel.TRUSTED TrustLevel.TRUSTED
} }
settingsMock.ignore.getHostClearInterval { 0 }
settingsMock.ignore.getHostHopelessInterval { 0 }
settingsMock.ignore.getHostRejectInterval { 0 }
initMocks() initMocks()
cache.onHostDiscoveredEvent(new HostDiscoveredEvent(destination: destinations.dest1)) cache.onHostDiscoveredEvent(new HostDiscoveredEvent(destination: destinations.dest1))
cache.onHostDiscoveredEvent(new HostDiscoveredEvent(destination: destinations.dest1)) cache.onHostDiscoveredEvent(new HostDiscoveredEvent(destination: destinations.dest1))
@@ -229,6 +262,11 @@ class HostCacheTest {
assert d == destinations.dest1 assert d == destinations.dest1
TrustLevel.TRUSTED TrustLevel.TRUSTED
} }
settingsMock.ignore.getHostClearInterval { 0 }
settingsMock.ignore.getHostHopelessInterval { 0 }
settingsMock.ignore.getHostRejectInterval { 0 }
initMocks() initMocks()
cache.onHostDiscoveredEvent(new HostDiscoveredEvent(destination: destinations.dest1)) cache.onHostDiscoveredEvent(new HostDiscoveredEvent(destination: destinations.dest1))
Thread.sleep(150) Thread.sleep(150)
@@ -260,6 +298,10 @@ class HostCacheTest {
TrustLevel.TRUSTED TrustLevel.TRUSTED
} }
settingsMock.ignore.getHostClearInterval { 0 }
settingsMock.ignore.getHostHopelessInterval { 0 }
settingsMock.ignore.getHostRejectInterval { 0 }
initMocks() initMocks()
def rv = cache.getHosts(5) def rv = cache.getHosts(5)
assert rv.size() == 1 assert rv.size() == 1

View File

@@ -9,6 +9,9 @@ import org.junit.Test
import com.muwire.core.InfoHash import com.muwire.core.InfoHash
import com.muwire.core.connection.Endpoint 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 { class UploaderTest {
@@ -52,7 +55,13 @@ class UploaderTest {
} }
private void startUpload() { 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 = new Thread(uploader.respond() as Runnable)
uploadThread.setDaemon(true) uploadThread.setDaemon(true)
uploadThread.start() uploadThread.start()
@@ -81,6 +90,7 @@ class UploaderTest {
startUpload() startUpload()
assert "200 OK" == readUntilRN() assert "200 OK" == readUntilRN()
assert "Content-Range: 0-19" == readUntilRN() assert "Content-Range: 0-19" == readUntilRN()
assert readUntilRN().startsWith("X-Have")
assert "" == readUntilRN() assert "" == readUntilRN()
byte [] data = new byte[20] byte [] data = new byte[20]
@@ -96,6 +106,7 @@ class UploaderTest {
startUpload() startUpload()
assert "200 OK" == readUntilRN() assert "200 OK" == readUntilRN()
assert "Content-Range: 5-15" == readUntilRN() assert "Content-Range: 5-15" == readUntilRN()
assert readUntilRN().startsWith("X-Have")
assert "" == readUntilRN() assert "" == readUntilRN()
byte [] data = new byte[11] byte [] data = new byte[11]
@@ -111,6 +122,7 @@ class UploaderTest {
request = new ContentRequest(range : new Range(0,20)) request = new ContentRequest(range : new Range(0,20))
startUpload() startUpload()
assert "416 Range Not Satisfiable" == readUntilRN() assert "416 Range Not Satisfiable" == readUntilRN()
assert readUntilRN().startsWith("X-Have")
assert "" == readUntilRN() assert "" == readUntilRN()
} }
@@ -123,6 +135,7 @@ class UploaderTest {
readUntilRN() readUntilRN()
readUntilRN() readUntilRN()
readUntilRN() readUntilRN()
readUntilRN()
byte [] data = new byte[length] byte [] data = new byte[length]
DataInputStream dis = new DataInputStream(is) DataInputStream dis = new DataInputStream(is)

View File

@@ -1,5 +1,6 @@
group = com.muwire group = com.muwire
version = 0.4.13 version = 0.5.2
i2pVersion = 0.9.42
groovyVersion = 2.4.15 groovyVersion = 2.4.15
slf4jVersion = 1.7.25 slf4jVersion = 1.7.25
spockVersion = 1.1-groovy-2.4 spockVersion = 1.1-groovy-2.4
@@ -12,7 +13,6 @@ targetCompatibility=1.8
# plugin properties # plugin properties
author = zab@mail.i2p author = zab@mail.i2p
signer = zab@mail.i2p signer = zab@mail.i2p
i2pVersion=0.9.41
keystorePassword=changeit keystorePassword=changeit
websiteURL=http://muwire.i2p websiteURL=http://muwire.i2p
updateURLsu3=http://muwire.i2p/MuWire.su3 updateURLsu3=http://muwire.i2p/MuWire.su3

View File

@@ -44,9 +44,9 @@ mainClassName = 'com.muwire.gui.Launcher'
applicationDefaultJvmArgs = ['-Djava.util.logging.config.file=logging.properties'] applicationDefaultJvmArgs = ['-Djava.util.logging.config.file=logging.properties']
apply from: 'gradle/publishing.gradle' apply from: 'gradle/publishing.gradle'
apply from: 'gradle/code-coverage.gradle' // apply from: 'gradle/code-coverage.gradle'
apply from: 'gradle/code-quality.gradle' // apply from: 'gradle/code-quality.gradle'
apply from: 'gradle/integration-test.gradle' // apply from: 'gradle/integration-test.gradle'
// apply from: 'gradle/package.gradle' // apply from: 'gradle/package.gradle'
apply from: 'gradle/docs.gradle' apply from: 'gradle/docs.gradle'
apply plugin: 'com.github.johnrengelman.shadow' apply plugin: 'com.github.johnrengelman.shadow'
@@ -58,7 +58,11 @@ dependencies {
compile project(":core") compile project(":core")
compile "org.codehaus.griffon:griffon-guice:${griffon.version}" compile "org.codehaus.griffon:griffon-guice:${griffon.version}"
runtime "org.slf4j:slf4j-simple:${slf4jVersion}" // runtime "org.slf4j:slf4j-simple:${slf4jVersion}"
runtime group: 'org.slf4j', name: 'slf4j-jdk14', version: "${slf4jVersion}"
runtime group: 'org.slf4j', name: 'slf4j-api', version: "${slf4jVersion}"
runtime group: 'org.slf4j', name: 'jul-to-slf4j', version: "${slf4jVersion}"
runtime "javax.annotation:javax.annotation-api:1.3.2" runtime "javax.annotation:javax.annotation-api:1.3.2"
testCompile "org.codehaus.griffon:griffon-fest-test:${griffon.version}" testCompile "org.codehaus.griffon:griffon-fest-test:${griffon.version}"
@@ -119,6 +123,7 @@ if (hasProperty('debugRun') && ((project.debugRun as boolean))) {
} }
} }
/*
task jacocoRootMerge(type: org.gradle.testing.jacoco.tasks.JacocoMerge, dependsOn: [test, jacocoTestReport, jacocoIntegrationTestReport]) { task jacocoRootMerge(type: org.gradle.testing.jacoco.tasks.JacocoMerge, dependsOn: [test, jacocoTestReport, jacocoIntegrationTestReport]) {
executionData = files(jacocoTestReport.executionData, jacocoIntegrationTestReport.executionData) executionData = files(jacocoTestReport.executionData, jacocoIntegrationTestReport.executionData)
destinationFile = file("${buildDir}/jacoco/root.exec") destinationFile = file("${buildDir}/jacoco/root.exec")
@@ -138,4 +143,5 @@ task jacocoRootReport(dependsOn: jacocoRootMerge, type: JacocoReport) {
xml.destination = file("${buildDir}/reports/jacoco/root/root.xml") xml.destination = file("${buildDir}/reports/jacoco/root/root.xml")
} }
} }
*/

View File

@@ -46,4 +46,19 @@ mvcGroups {
view = 'com.muwire.gui.ContentPanelView' view = 'com.muwire.gui.ContentPanelView'
controller = 'com.muwire.gui.ContentPanelController' 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'
}
} }

View File

@@ -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()
}
}

View File

@@ -0,0 +1,99 @@
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.BrowseStatus
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
if (e.status == BrowseStatus.FETCHING)
model.totalResults = e.totalResults
}
}
void onUIResultEvent(UIResultEvent e) {
runInsideUIAsync {
model.results << e
model.resultCount = model.results.size()
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)
}
}

View File

@@ -68,6 +68,16 @@ class ContentPanelController {
model.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 @ControllerAction
void trust() { void trust() {
int selectedHit = view.getSelectedHit() int selectedHit = view.getSelectedHit()

View File

@@ -13,10 +13,10 @@ import javax.annotation.Nonnull
import javax.inject.Inject import javax.inject.Inject
import javax.swing.JTable import javax.swing.JTable
import com.muwire.core.Constants
import com.muwire.core.Core import com.muwire.core.Core
import com.muwire.core.Persona import com.muwire.core.Persona
import com.muwire.core.SharedFile import com.muwire.core.SharedFile
import com.muwire.core.SplitPattern
import com.muwire.core.download.Downloader import com.muwire.core.download.Downloader
import com.muwire.core.download.DownloadStartedEvent import com.muwire.core.download.DownloadStartedEvent
import com.muwire.core.download.UIDownloadCancelledEvent import com.muwire.core.download.UIDownloadCancelledEvent
@@ -25,6 +25,7 @@ import com.muwire.core.download.UIDownloadPausedEvent
import com.muwire.core.download.UIDownloadResumedEvent import com.muwire.core.download.UIDownloadResumedEvent
import com.muwire.core.files.DirectoryUnsharedEvent import com.muwire.core.files.DirectoryUnsharedEvent
import com.muwire.core.files.FileUnsharedEvent import com.muwire.core.files.FileUnsharedEvent
import com.muwire.core.files.UIPersistFilesEvent
import com.muwire.core.search.QueryEvent import com.muwire.core.search.QueryEvent
import com.muwire.core.search.SearchEvent import com.muwire.core.search.SearchEvent
import com.muwire.core.trust.RemoteTrustList import com.muwire.core.trust.RemoteTrustList
@@ -77,16 +78,18 @@ class MainFrameController {
def searchEvent def searchEvent
if (hashSearch) { if (hashSearch) {
searchEvent = new SearchEvent(searchHash : root, uuid : uuid, oobInfohash: true) searchEvent = new SearchEvent(searchHash : root, uuid : uuid, oobInfohash: true, compressedResults : true)
} else { } else {
// this can be improved a lot // 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 terms = replaced.split(" ")
def nonEmpty = [] def nonEmpty = []
terms.each { if (it.length() > 0) nonEmpty << it } 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, compressedResults : true)
} }
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, replyTo: core.me.destination, receivedOn: core.me.destination,
originator : core.me)) originator : core.me))
} }
@@ -269,22 +272,25 @@ class MainFrameController {
} }
void unshareSelectedFile() { void unshareSelectedFile() {
SharedFile sf = view.selectedSharedFile() def sf = view.selectedSharedFiles()
if (sf == null) if (sf == null)
return return
core.eventBus.publish(new FileUnsharedEvent(unsharedFile : sf)) sf.each {
core.eventBus.publish(new FileUnsharedEvent(unsharedFile : it))
}
core.eventBus.publish(new UIPersistFilesEvent())
} }
void stopWatchingDirectory() { @ControllerAction
String directory = mvcGroup.view.getSelectedWatchedDirectory() void addComment() {
if (directory == null) def selectedFiles = view.selectedSharedFiles()
if (selectedFiles == null || selectedFiles.isEmpty())
return return
core.muOptions.watchedDirectories.remove(directory)
saveMuWireSettings() Map<String, Object> params = new HashMap<>()
core.eventBus.publish(new DirectoryUnsharedEvent(directory : new File(directory))) params['selectedFiles'] = selectedFiles
params['core'] = core
model.watched.remove(directory) mvcGroup.createMVCGroup("add-comment", "Add Comment", params)
builder.getVariable("watched-directories-table").model.fireTableDataChanged()
} }
void saveMuWireSettings() { void saveMuWireSettings() {

View File

@@ -31,6 +31,9 @@ class MuWireStatusController {
model.outgoingConnections = outgoing model.outgoingConnections = outgoing
model.knownHosts = core.hostCache.hosts.size() model.knownHosts = core.hostCache.hosts.size()
model.failingHosts = core.hostCache.countFailingHosts()
model.hopelessHosts = core.hostCache.countHopelessHosts()
model.sharedFiles = core.fileManager.fileToSharedFile.size() model.sharedFiles = core.fileManager.fileToSharedFile.size()

View File

@@ -70,6 +70,10 @@ class OptionsController {
model.updateCheckInterval = text model.updateCheckInterval = text
settings.updateCheckInterval = Integer.valueOf(text) settings.updateCheckInterval = Integer.valueOf(text)
boolean searchComments = view.searchCommentsCheckbox.model.isSelected()
model.searchComments = searchComments
settings.searchComments = searchComments
boolean autoDownloadUpdate = view.autoDownloadUpdateCheckbox.model.isSelected() boolean autoDownloadUpdate = view.autoDownloadUpdateCheckbox.model.isSelected()
model.autoDownloadUpdate = autoDownloadUpdate model.autoDownloadUpdate = autoDownloadUpdate
settings.autoDownloadUpdate = autoDownloadUpdate settings.autoDownloadUpdate = autoDownloadUpdate
@@ -78,9 +82,24 @@ class OptionsController {
boolean shareDownloaded = view.shareDownloadedCheckbox.model.isSelected() boolean shareDownloaded = view.shareDownloadedCheckbox.model.isSelected()
model.shareDownloadedFiles = shareDownloaded model.shareDownloadedFiles = shareDownloaded
settings.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
text = view.speedSmoothSecondsField.text
model.speedSmoothSeconds = Integer.valueOf(text)
settings.speedSmoothSeconds = Integer.valueOf(text)
String downloadLocation = model.downloadLocation String downloadLocation = model.downloadLocation
settings.downloadLocation = new File(downloadLocation) settings.downloadLocation = new File(downloadLocation)
String incompleteLocation = model.incompleteLocation
settings.incompleteLocation = new File(incompleteLocation)
if (settings.embeddedRouter) { if (settings.embeddedRouter) {
text = view.inBwField.text text = view.inBwField.text
@@ -96,6 +115,10 @@ class OptionsController {
model.onlyTrusted = onlyTrusted model.onlyTrusted = onlyTrusted
settings.setAllowUntrusted(!onlyTrusted) settings.setAllowUntrusted(!onlyTrusted)
boolean searchExtraHop = view.searchExtraHopCheckbox.model.isSelected()
model.searchExtraHop = searchExtraHop
settings.searchExtraHop = searchExtraHop
boolean trustLists = view.allowTrustListsCheckbox.model.isSelected() boolean trustLists = view.allowTrustListsCheckbox.model.isSelected()
model.trustLists = trustLists model.trustLists = trustLists
settings.allowTrustLists = trustLists settings.allowTrustLists = trustLists
@@ -119,10 +142,9 @@ class OptionsController {
text = view.fontField.text text = view.fontField.text
model.font = text model.font = text
uiSettings.font = text uiSettings.font = text
// boolean showMonitor = view.monitorCheckbox.model.isSelected() uiSettings.autoFontSize = model.automaticFontSize
// model.showMonitor = showMonitor uiSettings.fontSize = Integer.parseInt(view.fontSizeField.text)
// uiSettings.showMonitor = showMonitor
boolean clearCancelledDownloads = view.clearCancelledDownloadsCheckbox.model.isSelected() boolean clearCancelledDownloads = view.clearCancelledDownloadsCheckbox.model.isSelected()
model.clearCancelledDownloads = clearCancelledDownloads model.clearCancelledDownloads = clearCancelledDownloads
@@ -136,10 +158,6 @@ class OptionsController {
model.excludeLocalResult = excludeLocalResult model.excludeLocalResult = excludeLocalResult
uiSettings.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") File uiSettingsFile = new File(core.home, "gui.properties")
uiSettingsFile.withOutputStream { uiSettingsFile.withOutputStream {
uiSettings.write(it) uiSettings.write(it)
@@ -164,4 +182,26 @@ class OptionsController {
if (rv == JFileChooser.APPROVE_OPTION) if (rv == JFileChooser.APPROVE_OPTION)
model.downloadLocation = chooser.getSelectedFile().getAbsolutePath() model.downloadLocation = chooser.getSelectedFile().getAbsolutePath()
} }
@ControllerAction
void incompleteLocation() {
def chooser = new JFileChooser()
chooser.setFileHidingEnabled(false)
chooser.setDialogTitle("Select location for downloaded files")
chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY)
int rv = chooser.showOpenDialog(null)
if (rv == JFileChooser.APPROVE_OPTION)
model.incompleteLocation = chooser.getSelectedFile().getAbsolutePath()
}
@ControllerAction
void automaticFontAction() {
model.automaticFontSize = true
model.customFontSize = 12
}
@ControllerAction
void customFontAction() {
model.automaticFontSize = false
}
} }

View File

@@ -4,67 +4,121 @@ import griffon.core.artifact.GriffonController
import griffon.core.controller.ControllerAction import griffon.core.controller.ControllerAction
import griffon.inject.MVCMember import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor import griffon.metadata.ArtifactProviderFor
import net.i2p.data.Base64
import javax.annotation.Nonnull import javax.annotation.Nonnull
import com.muwire.core.Core import com.muwire.core.Core
import com.muwire.core.Persona
import com.muwire.core.download.UIDownloadEvent import com.muwire.core.download.UIDownloadEvent
import com.muwire.core.search.UIResultEvent
import com.muwire.core.trust.TrustEvent import com.muwire.core.trust.TrustEvent
import com.muwire.core.trust.TrustLevel import com.muwire.core.trust.TrustLevel
@ArtifactProviderFor(GriffonController) @ArtifactProviderFor(GriffonController)
class SearchTabController { class SearchTabController {
@MVCMember @Nonnull @MVCMember @Nonnull
SearchTabModel model SearchTabModel model
@MVCMember @Nonnull @MVCMember @Nonnull
SearchTabView view SearchTabView view
Core core Core core
private def selectedResult() { private def selectedResults() {
int row = view.resultsTable.getSelectedRow() int[] rows = view.resultsTable.getSelectedRows()
if (row == -1) if (rows.length == 0)
return null return null
def sortEvt = view.lastSortEvent def sortEvt = view.lastSortEvent
if (sortEvt != null) { if (sortEvt != null) {
row = view.resultsTable.rowSorter.convertRowIndexToModel(row) 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
} }
model.results[row]
}
@ControllerAction
void download() {
def result = selectedResult()
if (result == null)
return
if (!mvcGroup.parentGroup.model.canDownload(result.infohash)) @ControllerAction
return void download() {
def results = selectedResults()
if (results == null)
return
def file = new File(application.context.get("muwire-settings").downloadLocation, result.name) results.removeAll {
!mvcGroup.parentGroup.model.canDownload(it.infohash)
}
def resultsBucket = model.hashBucket[result.infohash] results.each { result ->
def sources = model.sourcesBucket[result.infohash] def file = new File(application.context.get("muwire-settings").downloadLocation, result.name)
core.eventBus.publish(new UIDownloadEvent(result : resultsBucket, sources: sources, target : file)) def resultsBucket = model.hashBucket[result.infohash]
mvcGroup.parentGroup.view.showDownloadsWindow.call() def sources = model.sourcesBucket[result.infohash]
}
@ControllerAction core.eventBus.publish(new UIDownloadEvent(result : resultsBucket, sources: sources,
void trust() { target : file, sequential : view.sequentialDownloadCheckbox.model.isSelected()))
int row = view.selectedSenderRow() }
if (row < 0) mvcGroup.parentGroup.view.showDownloadsWindow.call()
return }
def sender = model.senders[row]
core.eventBus.publish( new TrustEvent(persona : sender, level : TrustLevel.TRUSTED))
}
@ControllerAction @ControllerAction
void distrust() { void trust() {
int row = view.selectedSenderRow() int row = view.selectedSenderRow()
if (row < 0) if (row < 0)
return return
def sender = model.senders[row] def sender = model.senders[row]
core.eventBus.publish( new TrustEvent(persona : sender, level : TrustLevel.DISTRUSTED)) 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)
}
@ControllerAction
void showComment() {
int[] selectedRows = view.resultsTable.getSelectedRows()
if (selectedRows.length != 1)
return
if (view.lastSortEvent != null)
selectedRows[0] = view.resultsTable.rowSorter.convertRowIndexToModel(selectedRows[0])
UIResultEvent event = model.results[selectedRows[0]]
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)
}
}

View File

@@ -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()
}
}

View File

@@ -10,15 +10,19 @@ import com.muwire.gui.UISettings
import javax.annotation.Nonnull import javax.annotation.Nonnull
import javax.inject.Inject import javax.inject.Inject
import javax.swing.JLabel
import javax.swing.JTable import javax.swing.JTable
import javax.swing.LookAndFeel import javax.swing.LookAndFeel
import javax.swing.UIManager import javax.swing.UIManager
import javax.swing.plaf.FontUIResource
import static griffon.util.GriffonApplicationUtils.isMacOSX import static griffon.util.GriffonApplicationUtils.isMacOSX
import static groovy.swing.SwingBuilder.lookAndFeel import static groovy.swing.SwingBuilder.lookAndFeel
import java.awt.Font import java.awt.Font
import java.awt.Toolkit
import java.util.logging.Level import java.util.logging.Level
import java.util.logging.LogManager
@Log @Log
class Initialize extends AbstractLifecycleHandler { class Initialize extends AbstractLifecycleHandler {
@@ -29,6 +33,16 @@ class Initialize extends AbstractLifecycleHandler {
@Override @Override
void execute() { void execute() {
if (System.getProperty("java.util.logging.config.file") == null) {
log.info("No config file specified, so turning off most logging")
def names = LogManager.getLogManager().getLoggerNames()
while(names.hasMoreElements()) {
def name = names.nextElement()
LogManager.getLogManager().getLogger(name).setLevel(Level.SEVERE)
}
}
log.info "Loading home dir" log.info "Loading home dir"
def portableHome = System.getProperty("portable.home") def portableHome = System.getProperty("portable.home")
def home = portableHome == null ? def home = portableHome == null ?
@@ -52,25 +66,43 @@ class Initialize extends AbstractLifecycleHandler {
guiPropsFile.withInputStream { props.load(it) } guiPropsFile.withInputStream { props.load(it) }
uiSettings = new UISettings(props) uiSettings = new UISettings(props)
def lnf
log.info("settting user-specified lnf $uiSettings.lnf") log.info("settting user-specified lnf $uiSettings.lnf")
try { try {
lookAndFeel(uiSettings.lnf) lnf = lookAndFeel(uiSettings.lnf)
} catch (Throwable bad) { } catch (Throwable bad) {
log.log(Level.WARNING,"couldn't set desired look and feeel, switching to defaults", bad) log.log(Level.WARNING,"couldn't set desired look and feel, switching to defaults", bad)
uiSettings.lnf = lookAndFeel("system","gtk","metal").getID() lnf = lookAndFeel("system","gtk","metal")
uiSettings.lnf = lnf.getID()
} }
if (uiSettings.font != null) { if (uiSettings.font != null || uiSettings.autoFontSize || uiSettings.fontSize > 0) {
log.info("setting user-specified font $uiSettings.font")
Font font = new Font(uiSettings.font, Font.PLAIN, 12) FontUIResource defaultFont = lnf.getDefaults().getFont("Label.font")
def defaults = UIManager.getDefaults()
defaults.put("Button.font", font) String fontName
defaults.put("RadioButton.font", font) if (uiSettings.font != null)
defaults.put("Label.font", font) fontName = uiSettings.font
defaults.put("CheckBox.font", font) else
defaults.put("Table.font", font) fontName = defaultFont.getName()
defaults.put("TableHeader.font", font)
// TODO: add others 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 { } else {
Properties props = new Properties() Properties props = new Properties()

View File

@@ -45,9 +45,12 @@ class Ready extends AbstractLifecycleHandler {
props.load(it) props.load(it)
} }
props = new MuWireSettings(props) props = new MuWireSettings(props)
if (props.incompleteLocation == null)
props.incompleteLocation = new File(home, "incompletes")
} else { } else {
log.info("creating new properties") log.info("creating new properties")
props = new MuWireSettings() props = new MuWireSettings()
props.incompleteLocation = new File(home, "incompletes")
props.embeddedRouter = Boolean.parseBoolean(System.getProperties().getProperty("embeddedRouter")) props.embeddedRouter = Boolean.parseBoolean(System.getProperties().getProperty("embeddedRouter"))
props.updateType = System.getProperty("updateType","jar") props.updateType = System.getProperty("updateType","jar")
def nickname def nickname

View 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
}

View File

@@ -0,0 +1,21 @@
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
@Observable int totalResults
@Observable int resultCount
def results = []
}

View File

@@ -40,11 +40,15 @@ class ContentPanelModel {
} }
void refresh() { void refresh() {
int selectedRule = view.getSelectedRule()
rules.clear() rules.clear()
rules.addAll(contentManager.matchers) rules.addAll(contentManager.matchers)
hits.clear() hits.clear()
view.rulesTable.model.fireTableDataChanged() view.rulesTable.model.fireTableDataChanged()
view.hitsTable.model.fireTableDataChanged() view.hitsTable.model.fireTableDataChanged()
if (selectedRule >= 0) {
view.rulesTable.selectionModel.setSelectionInterval(selectedRule,selectedRule)
}
} }
void onContentControlEvent(ContentControlEvent e) { void onContentControlEvent(ContentControlEvent e) {

View File

@@ -1,6 +1,8 @@
package com.muwire.gui package com.muwire.gui
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import java.nio.file.Path
import java.nio.file.Paths
import java.util.Calendar import java.util.Calendar
import java.util.UUID import java.util.UUID
@@ -8,12 +10,16 @@ import javax.annotation.Nonnull
import javax.inject.Inject import javax.inject.Inject
import javax.swing.JOptionPane import javax.swing.JOptionPane
import javax.swing.JTable 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.Core
import com.muwire.core.InfoHash import com.muwire.core.InfoHash
import com.muwire.core.MuWireSettings import com.muwire.core.MuWireSettings
import com.muwire.core.Persona import com.muwire.core.Persona
import com.muwire.core.RouterDisconnectedEvent import com.muwire.core.RouterDisconnectedEvent
import com.muwire.core.SharedFile
import com.muwire.core.connection.ConnectionAttemptStatus import com.muwire.core.connection.ConnectionAttemptStatus
import com.muwire.core.connection.ConnectionEvent import com.muwire.core.connection.ConnectionEvent
import com.muwire.core.connection.DisconnectionEvent import com.muwire.core.connection.DisconnectionEvent
@@ -21,6 +27,7 @@ import com.muwire.core.content.ContentControlEvent
import com.muwire.core.download.DownloadStartedEvent import com.muwire.core.download.DownloadStartedEvent
import com.muwire.core.download.Downloader import com.muwire.core.download.Downloader
import com.muwire.core.files.AllFilesLoadedEvent import com.muwire.core.files.AllFilesLoadedEvent
import com.muwire.core.files.DirectoryUnsharedEvent
import com.muwire.core.files.FileDownloadedEvent import com.muwire.core.files.FileDownloadedEvent
import com.muwire.core.files.FileHashedEvent import com.muwire.core.files.FileHashedEvent
import com.muwire.core.files.FileHashingEvent import com.muwire.core.files.FileHashingEvent
@@ -57,6 +64,8 @@ class MainFrameModel {
FactoryBuilderSupport builder FactoryBuilderSupport builder
@MVCMember @Nonnull @MVCMember @Nonnull
MainFrameController controller MainFrameController controller
@MVCMember @Nonnull
MainFrameView view
@Inject @Nonnull GriffonApplication application @Inject @Nonnull GriffonApplication application
@Observable boolean coreInitialized = false @Observable boolean coreInitialized = false
@Observable boolean routerPresent @Observable boolean routerPresent
@@ -64,8 +73,11 @@ class MainFrameModel {
def results = new ConcurrentHashMap<>() def results = new ConcurrentHashMap<>()
def downloads = [] def downloads = []
def uploads = [] def uploads = []
def shared = [] boolean treeVisible = true
def watched = [] def shared
def sharedTree
def treeRoot
final Map<SharedFile, TreeNode> fileToNode = new HashMap<>()
def connectionList = [] def connectionList = []
def searches = new LinkedList() def searches = new LinkedList()
def trusted = [] def trusted = []
@@ -81,6 +93,7 @@ class MainFrameModel {
@Observable boolean pauseButtonEnabled @Observable boolean pauseButtonEnabled
@Observable boolean clearButtonEnabled @Observable boolean clearButtonEnabled
@Observable String resumeButtonText @Observable String resumeButtonText
@Observable boolean addCommentButtonEnabled
@Observable boolean subscribeButtonEnabled @Observable boolean subscribeButtonEnabled
@Observable boolean markNeutralFromTrustedButtonEnabled @Observable boolean markNeutralFromTrustedButtonEnabled
@Observable boolean markDistrustedButtonEnabled @Observable boolean markDistrustedButtonEnabled
@@ -109,13 +122,22 @@ class MainFrameModel {
void updateTablePreservingSelection(String tableName) { void updateTablePreservingSelection(String tableName) {
def downloadTable = builder.getVariable(tableName) def downloadTable = builder.getVariable(tableName)
int selectedRow = downloadTable.getSelectedRow() int selectedRow = downloadTable.getSelectedRow()
downloadTable.model.fireTableDataChanged() while(true) {
try {
downloadTable.model.fireTableDataChanged()
break
} catch (IllegalArgumentException iae) {} // caused by underlying model changing while table is sorted
}
downloadTable.selectionModel.setSelectionInterval(selectedRow,selectedRow) downloadTable.selectionModel.setSelectionInterval(selectedRow,selectedRow)
} }
void mvcGroupInit(Map<String, Object> args) { void mvcGroupInit(Map<String, Object> args) {
uiSettings = application.context.get("ui-settings") uiSettings = application.context.get("ui-settings")
shared = []
treeRoot = new DefaultMutableTreeNode()
sharedTree = new DefaultTreeModel(treeRoot)
Timer timer = new Timer("download-pumper", true) Timer timer = new Timer("download-pumper", true)
timer.schedule({ timer.schedule({
@@ -159,7 +181,6 @@ class MainFrameModel {
core = e.getNewValue() core = e.getNewValue()
routerPresent = core.router != null routerPresent = core.router != null
me = core.me.getHumanReadableName() me = core.me.getHumanReadableName()
core.eventBus.register(UIResultEvent.class, this)
core.eventBus.register(UIResultBatchEvent.class, this) core.eventBus.register(UIResultBatchEvent.class, this)
core.eventBus.register(DownloadStartedEvent.class, this) core.eventBus.register(DownloadStartedEvent.class, this)
core.eventBus.register(ConnectionEvent.class, this) core.eventBus.register(ConnectionEvent.class, this)
@@ -191,7 +212,7 @@ class MainFrameModel {
return return
int retryInterval = core.muOptions.downloadRetryInterval int retryInterval = core.muOptions.downloadRetryInterval
if (retryInterval > 0) { if (retryInterval > 0) {
retryInterval *= 60000 retryInterval *= 1000
long now = System.currentTimeMillis() long now = System.currentTimeMillis()
if (now - lastRetryTime > retryInterval) { if (now - lastRetryTime > retryInterval) {
lastRetryTime = now lastRetryTime = now
@@ -207,7 +228,7 @@ class MainFrameModel {
} }
} }
}, 60000, 60000) }, 1000, 1000)
runInsideUIAsync { runInsideUIAsync {
trusted.addAll(core.trustService.good.values()) trusted.addAll(core.trustService.good.values())
@@ -227,9 +248,7 @@ class MainFrameModel {
void onAllFilesLoadedEvent(AllFilesLoadedEvent e) { void onAllFilesLoadedEvent(AllFilesLoadedEvent e) {
runInsideUIAsync { runInsideUIAsync {
watched.addAll(core.muOptions.watchedDirectories) core.muOptions.watchedDirectories.each { core.eventBus.publish(new FileSharedEvent(file : new File(it))) }
builder.getVariable("watched-directories-table").model.fireTableDataChanged()
watched.each { core.eventBus.publish(new FileSharedEvent(file : new File(it))) }
core.muOptions.trustSubscriptions.each { core.muOptions.trustSubscriptions.each {
core.eventBus.publish(new TrustSubscriptionEvent(persona : it, subscribe : true)) core.eventBus.publish(new TrustSubscriptionEvent(persona : it, subscribe : true))
@@ -297,7 +316,6 @@ class MainFrameModel {
void onFileHashingEvent(FileHashingEvent e) { void onFileHashingEvent(FileHashingEvent e) {
runInsideUIAsync { runInsideUIAsync {
loadedFiles = shared.size()
hashingFile = e.hashingFile hashingFile = e.hashingFile
} }
} }
@@ -313,6 +331,8 @@ class MainFrameModel {
loadedFiles = shared.size() loadedFiles = shared.size()
JTable table = builder.getVariable("shared-files-table") JTable table = builder.getVariable("shared-files-table")
table.model.fireTableDataChanged() table.model.fireTableDataChanged()
insertIntoTree(e.sharedFile)
loadedFiles = fileToNode.size()
} }
} }
@@ -322,6 +342,8 @@ class MainFrameModel {
loadedFiles = shared.size() loadedFiles = shared.size()
JTable table = builder.getVariable("shared-files-table") JTable table = builder.getVariable("shared-files-table")
table.model.fireTableDataChanged() table.model.fireTableDataChanged()
insertIntoTree(e.loadedFile)
loadedFiles = fileToNode.size()
} }
} }
@@ -329,8 +351,26 @@ class MainFrameModel {
runInsideUIAsync { runInsideUIAsync {
shared.remove(e.unsharedFile) shared.remove(e.unsharedFile)
loadedFiles = shared.size() loadedFiles = shared.size()
JTable table = builder.getVariable("shared-files-table")
table.model.fireTableDataChanged() 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()
} }
} }
@@ -473,8 +513,44 @@ class MainFrameModel {
shared << e.downloadedFile shared << e.downloadedFile
JTable table = builder.getVariable("shared-files-table") JTable table = builder.getVariable("shared-files-table")
table.model.fireTableDataChanged() 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 { private static class UIConnection {
Destination destination Destination destination

View File

@@ -16,6 +16,8 @@ class MuWireStatusModel {
@Observable int incomingConnections @Observable int incomingConnections
@Observable int outgoingConnections @Observable int outgoingConnections
@Observable int knownHosts @Observable int knownHosts
@Observable int failingHosts
@Observable int hopelessHosts
@Observable int sharedFiles @Observable int sharedFiles
@Observable int downloads @Observable int downloads

View File

@@ -13,7 +13,12 @@ class OptionsModel {
@Observable String updateCheckInterval @Observable String updateCheckInterval
@Observable boolean autoDownloadUpdate @Observable boolean autoDownloadUpdate
@Observable boolean shareDownloadedFiles @Observable boolean shareDownloadedFiles
@Observable boolean shareHiddenFiles
@Observable String downloadLocation @Observable String downloadLocation
@Observable String incompleteLocation
@Observable boolean searchComments
@Observable boolean browseFiles
@Observable int speedSmoothSeconds
// i2p options // i2p options
@Observable String inboundLength @Observable String inboundLength
@@ -27,6 +32,8 @@ class OptionsModel {
@Observable boolean showMonitor @Observable boolean showMonitor
@Observable String lnf @Observable String lnf
@Observable String font @Observable String font
@Observable boolean automaticFontSize
@Observable int customFontSize
@Observable boolean clearCancelledDownloads @Observable boolean clearCancelledDownloads
@Observable boolean clearFinishedDownloads @Observable boolean clearFinishedDownloads
@Observable boolean excludeLocalResult @Observable boolean excludeLocalResult
@@ -38,6 +45,7 @@ class OptionsModel {
// trust options // trust options
@Observable boolean onlyTrusted @Observable boolean onlyTrusted
@Observable boolean searchExtraHop
@Observable boolean trustLists @Observable boolean trustLists
@Observable String trustListInterval @Observable String trustListInterval
@@ -48,7 +56,12 @@ class OptionsModel {
updateCheckInterval = settings.updateCheckInterval updateCheckInterval = settings.updateCheckInterval
autoDownloadUpdate = settings.autoDownloadUpdate autoDownloadUpdate = settings.autoDownloadUpdate
shareDownloadedFiles = settings.shareDownloadedFiles shareDownloadedFiles = settings.shareDownloadedFiles
shareHiddenFiles = settings.shareHiddenFiles
downloadLocation = settings.downloadLocation.getAbsolutePath() downloadLocation = settings.downloadLocation.getAbsolutePath()
incompleteLocation = settings.incompleteLocation.getAbsolutePath()
searchComments = settings.searchComments
browseFiles = settings.browseFiles
speedSmoothSeconds = settings.speedSmoothSeconds
Core core = application.context.get("core") Core core = application.context.get("core")
inboundLength = core.i2pOptions["inbound.length"] inboundLength = core.i2pOptions["inbound.length"]
@@ -62,6 +75,8 @@ class OptionsModel {
showMonitor = uiSettings.showMonitor showMonitor = uiSettings.showMonitor
lnf = uiSettings.lnf lnf = uiSettings.lnf
font = uiSettings.font font = uiSettings.font
automaticFontSize = uiSettings.autoFontSize
customFontSize = uiSettings.fontSize
clearCancelledDownloads = uiSettings.clearCancelledDownloads clearCancelledDownloads = uiSettings.clearCancelledDownloads
clearFinishedDownloads = uiSettings.clearFinishedDownloads clearFinishedDownloads = uiSettings.clearFinishedDownloads
excludeLocalResult = uiSettings.excludeLocalResult excludeLocalResult = uiSettings.excludeLocalResult
@@ -73,6 +88,7 @@ class OptionsModel {
} }
onlyTrusted = !settings.allowUntrusted() onlyTrusted = !settings.allowUntrusted()
searchExtraHop = settings.searchExtraHop
trustLists = settings.allowTrustLists trustLists = settings.allowTrustLists
trustListInterval = String.valueOf(settings.trustListInterval) trustListInterval = String.valueOf(settings.trustListInterval)
} }

View File

@@ -21,6 +21,8 @@ class SearchTabModel {
@Observable boolean downloadActionEnabled @Observable boolean downloadActionEnabled
@Observable boolean trustButtonsEnabled @Observable boolean trustButtonsEnabled
@Observable boolean browseActionEnabled
@Observable boolean viewCommentActionEnabled
Core core Core core
UISettings uiSettings UISettings uiSettings

View File

@@ -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
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 598 B

View 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()
}
}

View File

@@ -0,0 +1,201 @@
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.JLabel
import javax.swing.JMenuItem
import javax.swing.JPopupMenu
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.Toolkit
import java.awt.datatransfer.StringSelection
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
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()})
label(text : bind {model.totalResults == 0 ? "" : Math.round(model.resultCount * 100 / model.totalResults)+ "%"})
}
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
resultsTable.addMouseListener(new MouseAdapter() {
public void mouseReleased(MouseEvent e) {
if (e.isPopupTrigger())
showMenu(e)
}
public void mousePressed(MouseEvent e) {
if (e.isPopupTrigger())
showMenu(e)
}
})
})
}
private void showMenu(MouseEvent e) {
JPopupMenu menu = new JPopupMenu()
if (model.downloadActionEnabled) {
JMenuItem download = new JMenuItem("Download")
download.addActionListener({controller.download()})
menu.add(download)
}
if (model.viewCommentActionEnabled) {
JMenuItem viewComment = new JMenuItem("View Comment")
viewComment.addActionListener({controller.viewComment()})
menu.add(viewComment)
}
JMenuItem copyHash = new JMenuItem("Copy Hash To Clipboard")
copyHash.addActionListener({
List<UIResultEvent> results = selectedResults()
def hash = ""
for(Iterator<UIResultEvent> iter = results.iterator(); iter.hasNext();) {
UIResultEvent result = iter.next()
hash += Base64.encode(result.infohash.getRoot())
if (iter.hasNext())
hash += "\n"
}
copyString(hash)
})
menu.add(copyHash)
JMenuItem copyName = new JMenuItem("Copy Name To Clipboard")
copyName.addActionListener({
List<UIResultEvent> results = selectedResults()
def name = ""
for(Iterator<UIResultEvent> iter = results.iterator(); iter.hasNext();) {
UIResultEvent result = iter.next()
name += result.getName()
if (iter.hasNext())
name += "\n"
}
copyString(name)
})
menu.add(copyName)
menu.show(e.getComponent(), e.getX(), e.getY())
}
private static copyString(String s) {
StringSelection selection = new StringSelection(s)
def clipboard = Toolkit.getDefaultToolkit().getSystemClipboard()
clipboard.setContents(selection, null)
}
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
}
}

View File

@@ -84,6 +84,7 @@ class ContentPanelView {
} }
panel (constraints : BorderLayout.SOUTH) { panel (constraints : BorderLayout.SOUTH) {
button(text : "Refresh", refreshAction) button(text : "Refresh", refreshAction)
button(text : "Clear Hits", clearHitsAction)
button(text : "Trust", enabled : bind {model.trustButtonsEnabled}, trustAction) button(text : "Trust", enabled : bind {model.trustButtonsEnabled}, trustAction)
button(text : "Distrust", enabled : bind {model.trustButtonsEnabled}, distrustAction) button(text : "Distrust", enabled : bind {model.trustButtonsEnabled}, distrustAction)
} }

View File

@@ -7,8 +7,10 @@ import griffon.metadata.ArtifactProviderFor
import javax.swing.JDialog import javax.swing.JDialog
import javax.swing.JPanel import javax.swing.JPanel
import javax.swing.SwingConstants import javax.swing.SwingConstants
import javax.swing.border.TitledBorder
import java.awt.BorderLayout import java.awt.BorderLayout
import java.awt.GridBagConstraints
import java.awt.event.WindowAdapter import java.awt.event.WindowAdapter
import java.awt.event.WindowEvent import java.awt.event.WindowEvent
@@ -33,24 +35,40 @@ class I2PStatusView {
panel = builder.panel { panel = builder.panel {
gridBagLayout() gridBagLayout()
label(text : "Network status", constraints : gbc(gridx:0, gridy:0)) panel(border : titledBorder(title : "General", border : etchedBorder(), titlePosition : TitledBorder.TOP),
label(text : bind {model.networkStatus}, constraints : gbc(gridx: 1, gridy:0)) constraints : gbc(gridx: 0, gridy : 0, fill : GridBagConstraints.HORIZONTAL, weightx: 100)) {
label(text: "Floodfill", constraints : gbc(gridx: 0, gridy : 1)) gridBagLayout()
label(text : bind {model.floodfill}, constraints : gbc(gridx:1, gridy:1)) label(text : "Network status", constraints : gbc(gridx:0, gridy:0, anchor : GridBagConstraints.LINE_START, weightx: 100))
label(text : "NTCP Connections", constraints : gbc(gridx:0, gridy:2)) label(text : bind {model.networkStatus}, constraints : gbc(gridx: 1, gridy:0, anchor : GridBagConstraints.LINE_END))
label(text : bind {model.ntcpConnections}, constraints : gbc(gridx: 1, gridy:2)) label(text: "Floodfill", constraints : gbc(gridx: 0, gridy : 1, anchor: GridBagConstraints.LINE_START, weightx: 100))
label(text : "SSU Connections", constraints : gbc(gridx:0, gridy:3)) label(text : bind {model.floodfill}, constraints : gbc(gridx:1, gridy:1, anchor : GridBagConstraints.LINE_END))
label(text : bind {model.ssuConnections}, constraints : gbc(gridx: 1, gridy:3)) label(text : "Active Peers", constraints : gbc(gridx:0, gridy:2, anchor : GridBagConstraints.LINE_START, weightx: 100))
label(text : "Participating Tunnels", constraints : gbc(gridx:0, gridy:4)) label(text : bind {model.activePeers}, constraints : gbc(gridx: 1, gridy:2, anchor : GridBagConstraints.LINE_END))
label(text : bind {model.participatingTunnels}, constraints : gbc(gridx: 1, gridy:4)) }
label(text : "Participating Bandwidth", constraints : gbc(gridx:0, gridy:5)) panel(border : titledBorder(title : "Connections", border : etchedBorder(), titlePosition : TitledBorder.TOP),
label(text : bind {model.participatingBW}, constraints : gbc(gridx: 1, gridy:5)) constraints : gbc(gridx: 0, gridy: 1, fill : GridBagConstraints.HORIZONTAL, weightx: 100)) {
label(text : "Active Peers", constraints : gbc(gridx:0, gridy:6)) gridBagLayout()
label(text : bind {model.activePeers}, constraints : gbc(gridx: 1, gridy:6)) label(text : "NTCP", constraints : gbc(gridx:0, gridy:0, anchor: GridBagConstraints.LINE_START, weightx: 100))
label(text : "Receive Bps (15 seconds)", constraints : gbc(gridx:0, gridy:7)) label(text : bind {model.ntcpConnections}, constraints : gbc(gridx: 1, gridy:0, anchor : GridBagConstraints.LINE_END))
label(text : bind {model.receiveBps}, constraints : gbc(gridx: 1, gridy:7)) label(text : "SSU", constraints : gbc(gridx:0, gridy:1, anchor: GridBagConstraints.LINE_START, weightx: 100))
label(text : "Send Bps (15 seconds)", constraints : gbc(gridx:0, gridy:8)) label(text : bind {model.ssuConnections}, constraints : gbc(gridx: 1, gridy:1, anchor : GridBagConstraints.LINE_END))
label(text : bind {model.sendBps}, constraints : gbc(gridx: 1, gridy:8)) }
panel(border : titledBorder(title : "Participation", border : etchedBorder(), titlePosition : TitledBorder.TOP),
constraints : gbc(gridx: 0, gridy: 2, fill : GridBagConstraints.HORIZONTAL, weightx: 100)) {
gridBagLayout()
label(text : "Tunnels", constraints : gbc(gridx:0, gridy:0, anchor: GridBagConstraints.LINE_START, weightx: 100))
label(text : bind {model.participatingTunnels}, constraints : gbc(gridx: 1, gridy:0, anchor : GridBagConstraints.LINE_END))
label(text : "Bandwidth", constraints : gbc(gridx:0, gridy:1, anchor: GridBagConstraints.LINE_START, weightx: 100))
label(text : bind {model.participatingBW}, constraints : gbc(gridx: 1, gridy:1, anchor : GridBagConstraints.LINE_END))
}
panel(border : titledBorder(title : "Bandwidth", border : etchedBorder(), titlePosition : TitledBorder.TOP),
constraints : gbc(gridx: 0, gridy: 3, fill : GridBagConstraints.HORIZONTAL, weightx: 100)) {
gridBagLayout()
label(text : "Receive Bps (15 seconds)", constraints : gbc(gridx:0, gridy:0, anchor: GridBagConstraints.LINE_START, weightx: 100))
label(text : bind {model.receiveBps}, constraints : gbc(gridx: 1, gridy:0, anchor : GridBagConstraints.LINE_END))
label(text : "Send Bps (15 seconds)", constraints : gbc(gridx:0, gridy:1, anchor: GridBagConstraints.LINE_START, weightx: 100))
label(text : bind {model.sendBps}, constraints : gbc(gridx: 1, gridy:1, anchor : GridBagConstraints.LINE_END))
}
} }
buttonsPanel = builder.panel { buttonsPanel = builder.panel {

View File

@@ -16,13 +16,18 @@ import javax.swing.JMenuItem
import javax.swing.JPopupMenu import javax.swing.JPopupMenu
import javax.swing.JSplitPane import javax.swing.JSplitPane
import javax.swing.JTable import javax.swing.JTable
import javax.swing.JTree
import javax.swing.ListSelectionModel import javax.swing.ListSelectionModel
import javax.swing.SwingConstants import javax.swing.SwingConstants
import javax.swing.TransferHandler
import javax.swing.border.Border import javax.swing.border.Border
import javax.swing.table.DefaultTableCellRenderer import javax.swing.table.DefaultTableCellRenderer
import javax.swing.tree.TreeNode
import javax.swing.tree.TreePath
import com.muwire.core.Constants import com.muwire.core.Constants
import com.muwire.core.MuWireSettings import com.muwire.core.MuWireSettings
import com.muwire.core.SharedFile
import com.muwire.core.download.Downloader import com.muwire.core.download.Downloader
import com.muwire.core.files.FileSharedEvent import com.muwire.core.files.FileSharedEvent
import com.muwire.core.trust.RemoteTrustList import com.muwire.core.trust.RemoteTrustList
@@ -33,6 +38,7 @@ import java.awt.GridBagConstraints
import java.awt.GridBagLayout import java.awt.GridBagLayout
import java.awt.Insets import java.awt.Insets
import java.awt.Toolkit import java.awt.Toolkit
import java.awt.datatransfer.DataFlavor
import java.awt.datatransfer.StringSelection import java.awt.datatransfer.StringSelection
import java.awt.event.MouseAdapter import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent import java.awt.event.MouseEvent
@@ -53,16 +59,17 @@ class MainFrameView {
def downloadsTable def downloadsTable
def lastDownloadSortEvent def lastDownloadSortEvent
def lastSharedSortEvent def lastSharedSortEvent
def lastWatchedSortEvent
def trustTablesSortEvents = [:] def trustTablesSortEvents = [:]
UISettings settings
void initUI() { void initUI() {
UISettings settings = application.context.get("ui-settings") settings = application.context.get("ui-settings")
builder.with { builder.with {
application(size : [1024,768], id: 'main-frame', application(size : [1024,768], id: 'main-frame',
locationRelativeTo : null, locationRelativeTo : null,
title: application.configuration['application.title'] + " " + title: application.configuration['application.title'] + " " +
metadata["application.version"] + " revision " + metadata["build.revision"], metadata["application.version"] + " revision " + metadata["build.revision"],
iconImage: imageIcon('/MuWire-48x48.png').image, iconImage: imageIcon('/MuWire-48x48.png').image,
iconImages: [imageIcon('/MuWire-48x48.png').image, iconImages: [imageIcon('/MuWire-48x48.png').image,
imageIcon('/MuWire-32x32.png').image, imageIcon('/MuWire-32x32.png').image,
@@ -99,7 +106,7 @@ class MainFrameView {
panel(id: "top-panel", constraints: BorderLayout.CENTER) { panel(id: "top-panel", constraints: BorderLayout.CENTER) {
cardLayout() cardLayout()
label(constraints : "top-connect-panel", label(constraints : "top-connect-panel",
text : " MuWire is connecting, please wait. You will be able to search soon.") // TODO: real padding text : " MuWire is connecting, please wait. You will be able to search soon.") // TODO: real padding
panel(constraints : "top-search-panel") { panel(constraints : "top-search-panel") {
borderLayout() borderLayout()
panel(constraints: BorderLayout.CENTER) { panel(constraints: BorderLayout.CENTER) {
@@ -134,6 +141,15 @@ class MainFrameView {
closureColumn(header: "Speed", preferredWidth: 50, type:String, read :{row -> closureColumn(header: "Speed", preferredWidth: 50, type:String, read :{row ->
DataHelper.formatSize2Decimal(row.downloader.speed(), false) + "B/sec" DataHelper.formatSize2Decimal(row.downloader.speed(), false) + "B/sec"
}) })
closureColumn(header : "ETA", preferredWidth : 50, type:String, read :{ row ->
def speed = row.downloader.speed()
if (speed == 0)
return "Unknown"
else {
def remaining = (row.downloader.nPieces - row.downloader.donePieces()) * row.downloader.pieceSize / speed
return DataHelper.formatDuration(remaining.toLong() * 1000)
}
})
} }
} }
} }
@@ -158,8 +174,8 @@ class MainFrameView {
panel(constraints : "download-selected") { panel(constraints : "download-selected") {
gridBagLayout() gridBagLayout()
label(text : "Download Location:", constraints : gbc(gridx:0, gridy:0)) label(text : "Download Location:", constraints : gbc(gridx:0, gridy:0))
label(text : bind {model.downloader?.file?.getAbsolutePath()}, label(text : bind {model.downloader?.file?.getAbsolutePath()},
constraints: gbc(gridx:1, gridy:0, gridwidth: 2, insets : [0,0,0,20])) 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 : "Piece Size", constraints : gbc(gridx: 0, gridy:1))
label(text : bind {model.downloader?.pieceSize}, constraints : gbc(gridx:1, 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 : "Known Sources:", constraints : gbc(gridx:3, gridy: 0))
@@ -182,46 +198,55 @@ class MainFrameView {
borderLayout() borderLayout()
panel (constraints : BorderLayout.NORTH) { panel (constraints : BorderLayout.NORTH) {
label(text : bind { label(text : bind {
if (model.hashingFile == null) { if (model.hashingFile == null) {
"" "You can drag-and-drop files and directories here"
} else { } else {
"hashing: " + model.hashingFile.getAbsolutePath() + " (" + DataHelper.formatSize2Decimal(model.hashingFile.length(), false).toString() + "B)" "hashing: " + model.hashingFile.getAbsolutePath() + " (" + DataHelper.formatSize2Decimal(model.hashingFile.length(), false).toString() + "B)"
} }
}) })
} }
panel (border : etchedBorder(), constraints : BorderLayout.CENTER) { panel (border : etchedBorder(), constraints : BorderLayout.CENTER) {
gridLayout(cols : 2, rows : 1) borderLayout()
panel { panel (id : "shared-files-panel", constraints : BorderLayout.CENTER){
borderLayout() cardLayout()
scrollPane (constraints : BorderLayout.CENTER) { panel (constraints : "shared files table") {
table(id : "watched-directories-table", autoCreateRowSorter: true) { borderLayout()
tableModel(list : model.watched) { scrollPane(constraints : BorderLayout.CENTER) {
closureColumn(header: "Watched Directories", type : String, read : { it }) 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") {
panel { borderLayout()
borderLayout() scrollPane(constraints : BorderLayout.CENTER) {
scrollPane(constraints : BorderLayout.CENTER) { def jtree = new JTree(model.sharedTree)
table(id : "shared-files-table", autoCreateRowSorter: true) { jtree.setCellRenderer(new SharedTreeRenderer())
tableModel(list : model.shared) { tree(id : "shared-files-tree", rootVisible : false, expandsSelectedPaths: true, jtree)
closureColumn(header : "Name", preferredWidth : 500, type : String, read : {row -> row.getCachedPath()})
closureColumn(header : "Size", preferredWidth : 100, type : Long, read : {row -> row.getCachedLength() })
}
} }
} }
} }
} }
panel (constraints : BorderLayout.SOUTH) { panel (constraints : BorderLayout.SOUTH) {
gridLayout(rows:1, cols:2) gridLayout(rows:1, cols:3)
panel { panel {
button(text : "Add directories to watch", actionPerformed : watchDirectories) buttonGroup(id : "sharedViewType")
button(text : "Share files", actionPerformed : shareFiles) radioButton(text : "Tree", selected : true, buttonGroup : sharedViewType, actionPerformed : showSharedFilesTree)
radioButton(text : "Table", selected : false, buttonGroup : sharedViewType, actionPerformed : showSharedFilesTable)
} }
panel { panel {
label("Shared:") button(text : "Share files", actionPerformed : shareFiles)
label(text : bind {model.loadedFiles.toString()}) button(text : "Add Comment", enabled : bind {model.addCommentButtonEnabled}, addCommentAction)
}
panel {
panel {
label("Shared:")
label(text : bind {model.loadedFiles}, id : "shared-files-count")
}
} }
} }
} }
@@ -305,8 +330,8 @@ class MainFrameView {
}) })
closureColumn(header : "Timestamp", type : String, read : { closureColumn(header : "Timestamp", type : String, read : {
String.format("%02d", it.timestamp.get(Calendar.HOUR_OF_DAY)) + ":" + 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.MINUTE)) + ":" +
String.format("%02d", it.timestamp.get(Calendar.SECOND)) String.format("%02d", it.timestamp.get(Calendar.SECOND))
}) })
} }
} }
@@ -392,6 +417,22 @@ class MainFrameView {
} }
void mvcGroupInit(Map<String, String> args) { 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 downloadsTable = builder.getVariable("downloads-table")
def selectionModel = downloadsTable.getSelectionModel() def selectionModel = downloadsTable.getSelectionModel()
selectionModel.setSelectionMode(ListSelectionModel.SINGLE_SELECTION) selectionModel.setSelectionMode(ListSelectionModel.SINGLE_SELECTION)
@@ -407,7 +448,7 @@ class MainFrameView {
return return
} }
def downloader = model.downloads[selectedRow]?.downloader def downloader = model.downloads[selectedRow]?.downloader
if (downloader == null) if (downloader == null)
return return
model.downloader = downloader model.downloader = downloader
downloadDetailsPanel.getLayout().show(downloadDetailsPanel,"download-selected") downloadDetailsPanel.getLayout().show(downloadDetailsPanel,"download-selected")
@@ -415,26 +456,26 @@ class MainFrameView {
case Downloader.DownloadState.CONNECTING : case Downloader.DownloadState.CONNECTING :
case Downloader.DownloadState.DOWNLOADING : case Downloader.DownloadState.DOWNLOADING :
case Downloader.DownloadState.HASHLIST: case Downloader.DownloadState.HASHLIST:
model.cancelButtonEnabled = true model.cancelButtonEnabled = true
model.pauseButtonEnabled = true model.pauseButtonEnabled = true
model.retryButtonEnabled = false model.retryButtonEnabled = false
break break
case Downloader.DownloadState.FAILED: case Downloader.DownloadState.FAILED:
model.cancelButtonEnabled = true model.cancelButtonEnabled = true
model.retryButtonEnabled = true model.retryButtonEnabled = true
model.resumeButtonText = "Retry" model.resumeButtonText = "Retry"
model.pauseButtonEnabled = false model.pauseButtonEnabled = false
break break
case Downloader.DownloadState.PAUSED: case Downloader.DownloadState.PAUSED:
model.cancelButtonEnabled = true model.cancelButtonEnabled = true
model.retryButtonEnabled = true model.retryButtonEnabled = true
model.resumeButtonText = "Resume" model.resumeButtonText = "Resume"
model.pauseButtonEnabled = false model.pauseButtonEnabled = false
break break
default: default:
model.cancelButtonEnabled = false model.cancelButtonEnabled = false
model.retryButtonEnabled = false model.retryButtonEnabled = false
model.pauseButtonEnabled = false model.pauseButtonEnabled = false
} }
}) })
@@ -448,25 +489,19 @@ class MainFrameView {
downloadsTable.rowSorter.setComparator(2, new DownloaderComparator()) downloadsTable.rowSorter.setComparator(2, new DownloaderComparator())
downloadsTable.addMouseListener(new MouseAdapter() { downloadsTable.addMouseListener(new MouseAdapter() {
@Override @Override
public void mouseReleased(MouseEvent e) { public void mouseReleased(MouseEvent e) {
if (e.isPopupTrigger()) if (e.isPopupTrigger())
showDownloadsMenu(e) showDownloadsMenu(e)
} }
@Override @Override
public void mousePressed(MouseEvent e) { public void mousePressed(MouseEvent e) {
if (e.isPopupTrigger()) if (e.isPopupTrigger())
showDownloadsMenu(e) showDownloadsMenu(e)
} }
}) })
// shared files table
def sharedFilesTable = builder.getVariable("shared-files-table")
sharedFilesTable.columnModel.getColumn(1).setCellRenderer(new SizeRenderer())
sharedFilesTable.rowSorter.addRowSorterListener({evt -> lastSharedSortEvent = evt})
sharedFilesTable.rowSorter.setSortsOnUpdates(true)
// shared files menu
JPopupMenu sharedFilesMenu = new JPopupMenu() JPopupMenu sharedFilesMenu = new JPopupMenu()
JMenuItem copyHashToClipboard = new JMenuItem("Copy hash to clipboard") JMenuItem copyHashToClipboard = new JMenuItem("Copy hash to clipboard")
copyHashToClipboard.addActionListener({mvcGroup.view.copyHashToClipboard()}) copyHashToClipboard.addActionListener({mvcGroup.view.copyHashToClipboard()})
@@ -474,17 +509,46 @@ class MainFrameView {
JMenuItem unshareSelectedFiles = new JMenuItem("Unshare selected files") JMenuItem unshareSelectedFiles = new JMenuItem("Unshare selected files")
unshareSelectedFiles.addActionListener({mvcGroup.controller.unshareSelectedFile()}) unshareSelectedFiles.addActionListener({mvcGroup.controller.unshareSelectedFile()})
sharedFilesMenu.add(unshareSelectedFiles) sharedFilesMenu.add(unshareSelectedFiles)
sharedFilesTable.addMouseListener(new MouseAdapter() { JMenuItem commentSelectedFiles = new JMenuItem("Comment selected files")
@Override commentSelectedFiles.addActionListener({mvcGroup.controller.addComment()})
public void mouseReleased(MouseEvent e) { sharedFilesMenu.add(commentSelectedFiles)
if (e.isPopupTrigger())
showPopupMenu(sharedFilesMenu, e) def sharedFilesMouseListener = new MouseAdapter() {
} @Override
@Override public void mouseReleased(MouseEvent e) {
public void mousePressed(MouseEvent e) { if (e.isPopupTrigger())
if (e.isPopupTrigger()) showPopupMenu(sharedFilesMenu, e)
showPopupMenu(sharedFilesMenu, e) }
} @Override
public void mousePressed(MouseEvent e) {
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 // searches table
@@ -503,38 +567,17 @@ class MainFrameView {
searchTableMenu.add(distrustSearcher) searchTableMenu.add(distrustSearcher)
searchesTable.addMouseListener(new MouseAdapter() { searchesTable.addMouseListener(new MouseAdapter() {
@Override @Override
public void mouseReleased(MouseEvent e) { public void mouseReleased(MouseEvent e) {
if (e.isPopupTrigger()) if (e.isPopupTrigger())
showPopupMenu(searchTableMenu, e) showPopupMenu(searchTableMenu, e)
} }
@Override @Override
public void mousePressed(MouseEvent e) { public void mousePressed(MouseEvent e) {
if (e.isPopupTrigger()) if (e.isPopupTrigger())
showPopupMenu(searchTableMenu, e) showPopupMenu(searchTableMenu, e)
} }
}) })
// 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)
}
@Override
public void mousePressed(MouseEvent e) {
if (e.isPopupTrigger())
showPopupMenu(watchedMenu, e)
}
})
// subscription table // subscription table
def subscriptionTable = builder.getVariable("subscription-table") def subscriptionTable = builder.getVariable("subscription-table")
@@ -557,20 +600,20 @@ class MainFrameView {
switch(trustList.status) { switch(trustList.status) {
case RemoteTrustList.Status.NEW: case RemoteTrustList.Status.NEW:
case RemoteTrustList.Status.UPDATING: case RemoteTrustList.Status.UPDATING:
model.reviewButtonEnabled = false model.reviewButtonEnabled = false
model.updateButtonEnabled = false model.updateButtonEnabled = false
model.unsubscribeButtonEnabled = false model.unsubscribeButtonEnabled = false
break break
case RemoteTrustList.Status.UPDATED: case RemoteTrustList.Status.UPDATED:
model.reviewButtonEnabled = true model.reviewButtonEnabled = true
model.updateButtonEnabled = true model.updateButtonEnabled = true
model.unsubscribeButtonEnabled = true model.unsubscribeButtonEnabled = true
break break
case RemoteTrustList.Status.UPDATE_FAILED: case RemoteTrustList.Status.UPDATE_FAILED:
model.reviewButtonEnabled = false model.reviewButtonEnabled = false
model.updateButtonEnabled = true model.updateButtonEnabled = true
model.unsubscribeButtonEnabled = true model.unsubscribeButtonEnabled = true
break break
} }
}) })
@@ -609,28 +652,65 @@ class MainFrameView {
model.markNeutralFromDistrustedButtonEnabled = true model.markNeutralFromDistrustedButtonEnabled = true
} }
}) })
// show tree by default
showSharedFilesTree.call()
} }
private static void showPopupMenu(JPopupMenu menu, MouseEvent event) { private static void showPopupMenu(JPopupMenu menu, MouseEvent event) {
menu.show(event.getComponent(), event.getX(), event.getY()) menu.show(event.getComponent(), event.getX(), event.getY())
} }
def selectedSharedFile() { def selectedSharedFiles() {
def sharedFilesTable = builder.getVariable("shared-files-table") if (!model.treeVisible) {
int selected = sharedFilesTable.getSelectedRow() def sharedFilesTable = builder.getVariable("shared-files-table")
if (selected < 0) int[] selected = sharedFilesTable.getSelectedRows()
return null if (selected.length == 0)
if (lastSharedSortEvent != null) return null
selected = sharedFilesTable.rowSorter.convertRowIndexToModel(selected) List<SharedFile> rv = new ArrayList<>()
model.shared[selected] 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
}
} }
def copyHashToClipboard() { private static void getLeafs(TreeNode node, List<SharedFile> dest) {
def selected = selectedSharedFile() if (node.isLeaf()) {
if (selected == null) dest.add(node.getUserObject())
return return
String root = Base64.encode(selected.infoHash.getRoot()) }
StringSelection selection = new StringSelection(root) def children = node.children()
while(children.hasMoreElements()) {
getLeafs(children.nextElement(), dest)
}
}
def copyHashToClipboard() {
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())
roots += root
if (iterator.hasNext())
roots += "\n"
}
StringSelection selection = new StringSelection(roots)
def clipboard = Toolkit.getDefaultToolkit().getSystemClipboard() def clipboard = Toolkit.getDefaultToolkit().getSystemClipboard()
clipboard.setContents(selection, null) clipboard.setContents(selection, null)
} }
@@ -646,9 +726,12 @@ class MainFrameView {
} }
int selectedDownloaderRow() { int selectedDownloaderRow() {
int selected = builder.getVariable("downloads-table").getSelectedRow() def downloadsTable = builder.getVariable("downloads-table")
int selected = downloadsTable.getSelectedRow()
if (selected < 0)
return selected
if (lastDownloadSortEvent != null) if (lastDownloadSortEvent != null)
selected = lastDownloadSortEvent.convertPreviousRowIndexToModel(selected) selected = downloadsTable.rowSorter.convertRowIndexToModel(selected)
selected selected
} }
@@ -726,7 +809,7 @@ class MainFrameView {
model.monitorPaneButtonEnabled = true model.monitorPaneButtonEnabled = true
model.trustPaneButtonEnabled = true model.trustPaneButtonEnabled = true
} }
def showDownloadsWindow = { def showDownloadsWindow = {
def cardsPanel = builder.getVariable("cards-panel") def cardsPanel = builder.getVariable("cards-panel")
cardsPanel.getLayout().show(cardsPanel, "downloads window") cardsPanel.getLayout().show(cardsPanel, "downloads window")
@@ -766,44 +849,34 @@ class MainFrameView {
model.monitorPaneButtonEnabled = true model.monitorPaneButtonEnabled = true
model.trustPaneButtonEnabled = false 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 shareFiles = {
def chooser = new JFileChooser() def chooser = new JFileChooser()
chooser.setFileHidingEnabled(false) chooser.setFileHidingEnabled(!model.core.muOptions.shareHiddenFiles)
chooser.setDialogTitle("Select file to share") chooser.setDialogTitle("Select file to share")
chooser.setFileSelectionMode(JFileChooser.FILES_ONLY) chooser.setFileSelectionMode(JFileChooser.FILES_ONLY)
chooser.setMultiSelectionEnabled(true)
int rv = chooser.showOpenDialog(null) int rv = chooser.showOpenDialog(null)
if (rv == JFileChooser.APPROVE_OPTION) { 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()
if (selectedRow < 0)
return null
if (lastWatchedSortEvent != null)
selectedRow = watchedTable.rowSorter.convertRowIndexToModel(selectedRow)
model.watched[selectedRow]
}
int getSelectedTrustTablesRow(String tableName) { int getSelectedTrustTablesRow(String tableName) {
def table = builder.getVariable(tableName) def table = builder.getVariable(tableName)
int selectedRow = table.getSelectedRow() int selectedRow = table.getSelectedRow()
@@ -813,4 +886,12 @@ class MainFrameView {
selectedRow = table.rowSorter.convertRowIndexToModel(selectedRow) selectedRow = table.rowSorter.convertRowIndexToModel(selectedRow)
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()
}
} }

View File

@@ -7,10 +7,12 @@ import griffon.metadata.ArtifactProviderFor
import javax.swing.JDialog import javax.swing.JDialog
import javax.swing.JPanel import javax.swing.JPanel
import javax.swing.SwingConstants import javax.swing.SwingConstants
import javax.swing.border.TitledBorder
import com.muwire.core.Core import com.muwire.core.Core
import java.awt.BorderLayout import java.awt.BorderLayout
import java.awt.GridBagConstraints
import java.awt.event.WindowAdapter import java.awt.event.WindowAdapter
import java.awt.event.WindowEvent import java.awt.event.WindowEvent
@@ -35,16 +37,32 @@ class MuWireStatusView {
panel = builder.panel { panel = builder.panel {
gridBagLayout() gridBagLayout()
label(text : "Incoming connections", constraints : gbc(gridx:0, gridy:0)) panel(border : titledBorder(title : "Connections", border : etchedBorder(), titlePosition : TitledBorder.TOP),
label(text : bind {model.incomingConnections}, constraints : gbc(gridx:1, gridy:0)) constraints : gbc(gridx : 0, gridy: 0, fill : GridBagConstraints.HORIZONTAL, weightx: 100)) {
label(text : "Outgoing connections", constraints : gbc(gridx:0, gridy:1)) gridBagLayout()
label(text : bind {model.outgoingConnections}, constraints : gbc(gridx:1, gridy:1)) label(text : "Incoming", constraints : gbc(gridx:0, gridy:0, anchor : GridBagConstraints.LINE_START, weightx : 100))
label(text : "Known hosts", constraints : gbc(gridx:0, gridy:2)) label(text : bind {model.incomingConnections}, constraints : gbc(gridx:1, gridy:0, anchor : GridBagConstraints.LINE_END))
label(text : bind {model.knownHosts}, constraints : gbc(gridx:1, gridy:2)) label(text : "Outgoing", constraints : gbc(gridx:0, gridy:1, anchor : GridBagConstraints.LINE_START, weightx : 100))
label(text : "Shared files", constraints : gbc(gridx:0, gridy:3)) label(text : bind {model.outgoingConnections}, constraints : gbc(gridx:1, gridy:1, anchor : GridBagConstraints.LINE_END))
label(text : bind {model.sharedFiles}, constraints : gbc(gridx:1, gridy:3)) }
label(text : "Downloads", constraints : gbc(gridx:0, gridy:4)) panel(border : titledBorder(title : "Hosts", border : etchedBorder(), titlePosition : TitledBorder.TOP),
label(text : bind {model.downloads}, constraints : gbc(gridx:1, gridy:4)) constraints : gbc(gridx : 0, gridy : 1, fill : GridBagConstraints.HORIZONTAL, weightx: 100)) {
gridBagLayout()
label(text : "Known", constraints : gbc(gridx:0, gridy:0, anchor : GridBagConstraints.LINE_START, weightx : 100))
label(text : bind {model.knownHosts}, constraints : gbc(gridx:1, gridy:0, anchor : GridBagConstraints.LINE_END))
label(text : "Failing", constraints : gbc(gridx:0, gridy:1, anchor : GridBagConstraints.LINE_START, weightx : 100))
label(text : bind {model.failingHosts}, constraints : gbc(gridx:1, gridy:1, anchor : GridBagConstraints.LINE_END))
label(text : "Hopeless", constraints : gbc(gridx:0, gridy:2, anchor : GridBagConstraints.LINE_START, weightx : 100))
label(text : bind {model.hopelessHosts}, constraints : gbc(gridx:1, gridy:2, anchor : GridBagConstraints.LINE_END))
}
panel(border : titledBorder(title : "Files", border : etchedBorder(), titlePosition : TitledBorder.TOP),
constraints : gbc(gridx : 0, gridy : 2, fill : GridBagConstraints.HORIZONTAL, weightx: 100)) {
gridBagLayout()
label(text : "Shared", constraints : gbc(gridx:0, gridy:0, anchor : GridBagConstraints.LINE_START, weightx : 100))
label(text : bind {model.sharedFiles}, constraints : gbc(gridx:1, gridy:0, anchor : GridBagConstraints.LINE_END))
label(text : "Downloading", constraints : gbc(gridx:0, gridy:1, anchor : GridBagConstraints.LINE_START, weightx : 100))
label(text : bind {model.downloads}, constraints : gbc(gridx:1, gridy:1, anchor : GridBagConstraints.LINE_END))
}
} }
buttonsPanel = builder.panel { buttonsPanel = builder.panel {
gridBagLayout() gridBagLayout()
@@ -60,7 +78,8 @@ class MuWireStatusView {
statusPanel.add(buttonsPanel, BorderLayout.SOUTH) statusPanel.add(buttonsPanel, BorderLayout.SOUTH)
dialog.getContentPane().add(statusPanel) dialog.getContentPane().add(statusPanel)
dialog.pack() dialog.setSize(200,300)
dialog.setResizable(false)
dialog.setLocationRelativeTo(mainFrame) dialog.setLocationRelativeTo(mainFrame)
dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE) dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE)
dialog.addWindowListener(new WindowAdapter() { dialog.addWindowListener(new WindowAdapter() {

View File

@@ -3,15 +3,18 @@ package com.muwire.gui
import griffon.core.artifact.GriffonView import griffon.core.artifact.GriffonView
import griffon.inject.MVCMember import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor import griffon.metadata.ArtifactProviderFor
import groovy.swing.factory.TitledBorderFactory
import javax.swing.JDialog import javax.swing.JDialog
import javax.swing.JPanel import javax.swing.JPanel
import javax.swing.JTabbedPane import javax.swing.JTabbedPane
import javax.swing.SwingConstants import javax.swing.SwingConstants
import javax.swing.border.TitledBorder
import com.muwire.core.Core import com.muwire.core.Core
import java.awt.BorderLayout import java.awt.BorderLayout
import java.awt.GridBagConstraints
import java.awt.event.WindowAdapter import java.awt.event.WindowAdapter
import java.awt.event.WindowEvent import java.awt.event.WindowEvent
@@ -35,6 +38,10 @@ class OptionsView {
def updateField def updateField
def autoDownloadUpdateCheckbox def autoDownloadUpdateCheckbox
def shareDownloadedCheckbox def shareDownloadedCheckbox
def shareHiddenCheckbox
def searchCommentsCheckbox
def browseFilesCheckbox
def speedSmoothSecondsField
def inboundLengthField def inboundLengthField
def inboundQuantityField def inboundQuantityField
@@ -46,6 +53,7 @@ class OptionsView {
def lnfField def lnfField
def monitorCheckbox def monitorCheckbox
def fontField def fontField
def fontSizeField
def clearCancelledDownloadsCheckbox def clearCancelledDownloadsCheckbox
def clearFinishedDownloadsCheckbox def clearFinishedDownloadsCheckbox
def excludeLocalResultCheckbox def excludeLocalResultCheckbox
@@ -55,6 +63,7 @@ class OptionsView {
def outBwField def outBwField
def allowUntrustedCheckbox def allowUntrustedCheckbox
def searchExtraHopCheckbox
def allowTrustListsCheckbox def allowTrustListsCheckbox
def trustListIntervalField def trustListIntervalField
@@ -68,81 +77,156 @@ class OptionsView {
d.setResizable(false) d.setResizable(false)
p = builder.panel { p = builder.panel {
gridBagLayout() gridBagLayout()
label(text : "Retry failed downloads every", constraints : gbc(gridx: 0, gridy: 0)) panel (border : titledBorder(title : "Search Settings", border : etchedBorder(), titlePosition : TitledBorder.TOP),
retryField = textField(text : bind { model.downloadRetryInterval }, columns : 2, constraints : gbc(gridx: 1, gridy: 0)) constraints : gbc(gridx: 0, gridy : 0, fill : GridBagConstraints.HORIZONTAL)) {
label(text : "minutes", constraints : gbc(gridx : 2, gridy: 0)) gridBagLayout()
label(text : "Search in comments", constraints:gbc(gridx: 0, gridy:0, anchor : GridBagConstraints.LINE_START,
fill : GridBagConstraints.HORIZONTAL, weightx: 100))
searchCommentsCheckbox = checkBox(selected : bind {model.searchComments}, constraints : gbc(gridx:1, gridy:0,
anchor : GridBagConstraints.LINE_END, fill : GridBagConstraints.HORIZONTAL, weightx: 0))
label(text : "Allow browsing", constraints : gbc(gridx : 0, gridy : 1, anchor : GridBagConstraints.LINE_START,
fill : GridBagConstraints.HORIZONTAL, weightx: 100))
browseFilesCheckbox = checkBox(selected : bind {model.browseFiles}, constraints : gbc(gridx : 1, gridy : 1,
anchor : GridBagConstraints.LINE_END, fill : GridBagConstraints.HORIZONTAL, weightx: 0))
}
panel (border : titledBorder(title : "Download Settings", border : etchedBorder(), titlePosition : TitledBorder.TOP,
constraints : gbc(gridx : 0, gridy : 1, fill : GridBagConstraints.HORIZONTAL))) {
gridBagLayout()
label(text : "Retry failed downloads every (seconds)", constraints : gbc(gridx: 0, gridy: 0, anchor : GridBagConstraints.LINE_START, weightx: 100))
retryField = textField(text : bind { model.downloadRetryInterval }, columns : 2,
constraints : gbc(gridx: 2, gridy: 0, anchor : GridBagConstraints.LINE_END, weightx: 0))
label(text : "Save downloaded files to:", constraints: gbc(gridx:0, gridy:1, anchor : GridBagConstraints.LINE_START))
label(text : bind {model.downloadLocation}, constraints: gbc(gridx:1, gridy:1, anchor : GridBagConstraints.LINE_START))
button(text : "Choose", constraints : gbc(gridx : 2, gridy:1), downloadLocationAction)
label(text : "Store incomplete files in:", constraints: gbc(gridx:0, gridy:2, anchor : GridBagConstraints.LINE_START))
label(text : bind {model.incompleteLocation}, constraints: gbc(gridx:1, gridy:2, anchor : GridBagConstraints.LINE_START))
button(text : "Choose", constraints : gbc(gridx : 2, gridy:2), incompleteLocationAction)
}
panel (border : titledBorder(title : "Sharing Settings", border : etchedBorder(), titlePosition : TitledBorder.TOP,
constraints : gbc(gridx : 0, gridy : 2, fill : GridBagConstraints.HORIZONTAL))) {
gridBagLayout()
label(text : "Share downloaded files", constraints : gbc(gridx : 0, gridy:0, anchor : GridBagConstraints.LINE_START, weightx : 100))
shareDownloadedCheckbox = checkBox(selected : bind {model.shareDownloadedFiles}, constraints : gbc(gridx :1, gridy:0, weightx : 0))
label(text : "Share hidden files", constraints : gbc(gridx : 0, gridy:1, anchor : GridBagConstraints.LINE_START, weightx : 100))
shareHiddenCheckbox = checkBox(selected : bind {model.shareHiddenFiles}, constraints : gbc(gridx :1, gridy:1, weightx : 0))
}
panel (border : titledBorder(title : "Update Settings", border : etchedBorder(), titlePosition : TitledBorder.TOP,
constraints : gbc(gridx : 0, gridy : 3, fill : GridBagConstraints.HORIZONTAL))) {
gridBagLayout()
label(text : "Check for updates every (hours)", constraints : gbc(gridx : 0, gridy: 0, anchor : GridBagConstraints.LINE_START, weightx : 100))
updateField = textField(text : bind {model.updateCheckInterval }, columns : 2, constraints : gbc(gridx : 1, gridy: 0, weightx: 0))
label(text : "Check for updates every", constraints : gbc(gridx : 0, gridy: 1)) label(text : "Download updates automatically", constraints: gbc(gridx :0, gridy : 1, anchor : GridBagConstraints.LINE_START, weightx: 100))
updateField = textField(text : bind {model.updateCheckInterval }, columns : 2, constraints : gbc(gridx : 1, gridy: 1)) autoDownloadUpdateCheckbox = checkBox(selected : bind {model.autoDownloadUpdate},
label(text : "hours", constraints : gbc(gridx: 2, gridy : 1)) constraints : gbc(gridx:1, gridy : 1, anchor : GridBagConstraints.LINE_END))
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 { i = builder.panel {
gridBagLayout() gridBagLayout()
label(text : "Changing these settings requires a restart", constraints : gbc(gridx : 0, gridy : 0, gridwidth: 2)) label(text : "Changing any I2P settings requires a restart", constraints : gbc(gridx:0, gridy : 0))
label(text : "Inbound Length", constraints : gbc(gridx:0, gridy:1)) panel (border : titledBorder(title : "Tunnel Settings", border : etchedBorder(), titlePosition: TitledBorder.TOP,
inboundLengthField = textField(text : bind {model.inboundLength}, columns : 2, constraints : gbc(gridx:1, gridy:1)) constraints : gbc(gridx: 0, gridy: 1, fill : GridBagConstraints.HORIZONTAL, weightx : 100))) {
label(text : "Inbound Quantity", constraints : gbc(gridx:0, gridy:2)) gridBagLayout()
inboundQuantityField = textField(text : bind {model.inboundQuantity}, columns : 2, constraints : gbc(gridx:1, gridy:2)) label(text : "Inbound length", constraints : gbc(gridx:0, gridy:0, anchor : GridBagConstraints.LINE_START, weightx : 100))
label(text : "Outbound Length", constraints : gbc(gridx:0, gridy:3)) inboundLengthField = textField(text : bind {model.inboundLength}, columns : 2, constraints : gbc(gridx:1, gridy:0,
outboundLengthField = textField(text : bind {model.outboundLength}, columns : 2, constraints : gbc(gridx:1, gridy:3)) anchor : GridBagConstraints.LINE_END))
label(text : "Outbound Quantity", constraints : gbc(gridx:0, gridy:4)) label(text : "Inbound quantity", constraints : gbc(gridx:0, gridy:1, anchor : GridBagConstraints.LINE_START, weightx : 100))
outboundQuantityField = textField(text : bind {model.outboundQuantity}, columns : 2, constraints : gbc(gridx:1, gridy:4)) inboundQuantityField = textField(text : bind {model.inboundQuantity}, columns : 2, constraints : gbc(gridx:1, gridy:1,
anchor : GridBagConstraints.LINE_END))
label(text : "Outbound length", constraints : gbc(gridx:0, gridy:2, anchor : GridBagConstraints.LINE_START, weightx : 100))
outboundLengthField = textField(text : bind {model.outboundLength}, columns : 2, constraints : gbc(gridx:1, gridy:2,
anchor : GridBagConstraints.LINE_END))
label(text : "Outbound quantity", constraints : gbc(gridx:0, gridy:3, anchor : GridBagConstraints.LINE_START, weightx : 100))
outboundQuantityField = textField(text : bind {model.outboundQuantity}, columns : 2, constraints : gbc(gridx:1, gridy:3,
anchor : GridBagConstraints.LINE_END))
}
Core core = application.context.get("core") Core core = application.context.get("core")
if (core.router != null) { if (core.router != null) {
label(text : "TCP Port", constraints : gbc(gridx :0, gridy: 5)) panel(border : titledBorder(title : "Port Settings", border : etchedBorder(), titlePosition : TitledBorder.TOP,
i2pNTCPPortField = textField(text : bind {model.i2pNTCPPort}, columns : 4, constraints : gbc(gridx:1, gridy:5)) constraints : gbc(gridx: 0, gridy : 2, fill : GridBagConstraints.HORIZONTAL, weightx: 100))) {
label(text : "UDP Port", constraints : gbc(gridx :0, gridy: 6)) gridBagLayout()
i2pUDPPortField = textField(text : bind {model.i2pUDPPort}, columns : 4, constraints : gbc(gridx:1, gridy:6)) label(text : "TCP port", constraints : gbc(gridx :0, gridy: 0, anchor : GridBagConstraints.LINE_START, weightx : 100))
i2pNTCPPortField = textField(text : bind {model.i2pNTCPPort}, columns : 4, constraints : gbc(gridx:1, gridy:0, anchor : GridBagConstraints.LINE_END))
label(text : "UDP port", constraints : gbc(gridx :0, gridy: 1, anchor : GridBagConstraints.LINE_START, weightx : 100))
i2pUDPPortField = textField(text : bind {model.i2pUDPPort}, columns : 4, constraints : gbc(gridx:1, gridy:1, anchor : GridBagConstraints.LINE_END))
}
} }
panel(constraints : gbc(gridx: 0, gridy: 3, weighty: 100))
} }
u = builder.panel { u = builder.panel {
gridBagLayout() gridBagLayout()
label(text : "Changing these settings requires a restart", constraints : gbc(gridx : 0, gridy : 0, gridwidth: 2)) panel (border : titledBorder(title : "Theme Settings (changes require restart)", border : etchedBorder(), titlePosition: TitledBorder.TOP,
label(text : "Look And Feel", constraints : gbc(gridx: 0, gridy:1)) constraints : gbc(gridx : 0, gridy : 0, fill : GridBagConstraints.HORIZONTAL, weightx : 100))) {
lnfField = textField(text : bind {model.lnf}, columns : 4, constraints : gbc(gridx : 1, gridy : 1)) gridBagLayout()
label(text : "Font", constraints : gbc(gridx: 0, gridy : 2)) label(text : "Look And Feel", constraints : gbc(gridx: 0, gridy:0, anchor : GridBagConstraints.LINE_START, weightx: 100))
fontField = textField(text : bind {model.font}, columns : 4, constraints : gbc(gridx : 1, gridy:2)) lnfField = textField(text : bind {model.lnf}, columns : 4, constraints : gbc(gridx : 3, gridy : 0, anchor : GridBagConstraints.LINE_START))
// label(text : "Show Monitor", constraints : gbc(gridx :0, gridy: 3)) label(text : "Font", constraints : gbc(gridx: 0, gridy : 1, anchor : GridBagConstraints.LINE_START, weightx: 100))
// monitorCheckbox = checkBox(selected : bind {model.showMonitor}, constraints : gbc(gridx : 1, gridy: 3)) fontField = textField(text : bind {model.font}, columns : 4, constraints : gbc(gridx : 3, gridy:1, anchor : GridBagConstraints.LINE_START))
label(text : "Automatically Clear Cancelled Downloads", constraints: gbc(gridx: 0, gridy:4))
clearCancelledDownloadsCheckbox = checkBox(selected : bind {model.clearCancelledDownloads}, constraints : gbc(gridx : 1, gridy:4)) label(text : "Font size", constraints : gbc(gridx: 0, gridy : 2, anchor : GridBagConstraints.LINE_START, weightx : 100))
label(text : "Automatically Clear Finished Downloads", constraints: gbc(gridx: 0, gridy:5)) buttonGroup(id: "fontSizeGroup")
clearFinishedDownloadsCheckbox = checkBox(selected : bind {model.clearFinishedDownloads}, constraints : gbc(gridx : 1, gridy:5)) radioButton(text: "Automatic", selected : bind {model.automaticFontSize}, buttonGroup : fontSizeGroup,
label(text : "Exclude Local Files From Results", constraints: gbc(gridx:0, gridy:6)) constraints : gbc(gridx : 1, gridy: 2, anchor : GridBagConstraints.LINE_START), automaticFontAction)
excludeLocalResultCheckbox = checkBox(selected : bind {model.excludeLocalResult}, constraints : gbc(gridx: 1, gridy : 6)) radioButton(text: "Custom", selected : bind {!model.automaticFontSize}, buttonGroup : fontSizeGroup,
// label(text : "Show Hash Searches In Monitor", constraints: gbc(gridx:0, gridy:7)) constraints : gbc(gridx : 2, gridy: 2, anchor : GridBagConstraints.LINE_START), customFontAction)
// showSearchHashesCheckbox = checkBox(selected : bind {model.showSearchHashes}, constraints : gbc(gridx: 1, gridy: 7)) fontSizeField = textField(text : bind {model.customFontSize}, enabled : bind {!model.automaticFontSize},
constraints : gbc(gridx : 3, gridy : 2, anchor : GridBagConstraints.LINE_END))
}
panel (border : titledBorder(title : "Other Settings", border : etchedBorder(), titlePosition : TitledBorder.TOP),
constraints : gbc(gridx : 0, gridy : 1, fill : GridBagConstraints.HORIZONTAL, weightx : 100)) {
gridBagLayout()
label(text : "Automatically clear cancelled downloads", constraints: gbc(gridx: 0, gridy:0, anchor : GridBagConstraints.LINE_START, weightx: 100))
clearCancelledDownloadsCheckbox = checkBox(selected : bind {model.clearCancelledDownloads},
constraints : gbc(gridx : 1, gridy:0, anchor : GridBagConstraints.LINE_END))
label(text : "Automatically flear finished downloads", constraints: gbc(gridx: 0, gridy:1, anchor : GridBagConstraints.LINE_START, weightx: 100))
clearFinishedDownloadsCheckbox = checkBox(selected : bind {model.clearFinishedDownloads},
constraints : gbc(gridx : 1, gridy:1, anchor : GridBagConstraints.LINE_END))
label(text : "Smooth download speed over (seconds)", constraints : gbc(gridx: 0, gridy : 2, anchor : GridBagConstraints.LINE_START, weightx: 100))
speedSmoothSecondsField = textField(text : bind {model.speedSmoothSeconds},
constraints : gbc(gridx:1, gridy: 2, anchor : GridBagConstraints.LINE_START))
label(text : "Exclude local files from results", constraints: gbc(gridx:0, gridy:3, anchor : GridBagConstraints.LINE_START, weightx: 100))
excludeLocalResultCheckbox = checkBox(selected : bind {model.excludeLocalResult},
constraints : gbc(gridx: 1, gridy : 3, anchor : GridBagConstraints.LINE_END))
}
panel (constraints : gbc(gridx: 0, gridy: 2, weighty: 100))
} }
bandwidth = builder.panel { bandwidth = builder.panel {
gridBagLayout() gridBagLayout()
label(text : "Changing these settings requires a restart", constraints : gbc(gridx : 0, gridy : 0, gridwidth: 2)) panel( border : titledBorder(title : "Changing bandwidth settings requires a restart", border : etchedBorder(), titlePosition : TitledBorder.TOP),
label(text : "Inbound bandwidth (KB)", constraints : gbc(gridx: 0, gridy : 1)) constraints : gbc(gridx : 0, gridy : 0, fill : GridBagConstraints.HORIZONTAL, weightx : 100)) {
inBwField = textField(text : bind {model.inBw}, columns : 3, constraints : gbc(gridx : 1, gridy : 1)) gridBagLayout()
label(text : "Outbound bandwidth (KB)", constraints : gbc(gridx: 0, gridy : 2)) label(text : "Inbound bandwidth (KB)", constraints : gbc(gridx: 0, gridy : 0, anchor : GridBagConstraints.LINE_START, weightx : 100))
outBwField = textField(text : bind {model.outBw}, columns : 3, constraints : gbc(gridx : 1, gridy : 2)) inBwField = textField(text : bind {model.inBw}, columns : 3, constraints : gbc(gridx : 1, gridy : 0, anchor : GridBagConstraints.LINE_END))
label(text : "Outbound bandwidth (KB)", constraints : gbc(gridx: 0, gridy : 1, anchor : GridBagConstraints.LINE_START, weightx : 100))
outBwField = textField(text : bind {model.outBw}, columns : 3, constraints : gbc(gridx : 1, gridy : 1, anchor : GridBagConstraints.LINE_END))
}
panel(constraints : gbc(gridx: 0, gridy: 1, weighty: 100))
} }
trust = builder.panel { trust = builder.panel {
gridBagLayout() gridBagLayout()
label(text : "Allow only trusted connections", constraints : gbc(gridx: 0, gridy : 0)) panel (border : titledBorder(title : "Trust Settings", border : etchedBorder(), titlePosition : TitledBorder.TOP),
allowUntrustedCheckbox = checkBox(selected : bind {model.onlyTrusted}, constraints : gbc(gridx: 1, gridy : 0)) constraints : gbc(gridx : 0, gridy : 0, fill : GridBagConstraints.HORIZONTAL, weightx: 100)) {
label(text : "Allow others to view my trust list", constraints : gbc(gridx: 0, gridy : 1)) gridBagLayout()
allowTrustListsCheckbox = checkBox(selected : bind {model.trustLists}, constraints : gbc(gridx: 1, gridy : 1)) label(text : "Allow only trusted connections", constraints : gbc(gridx: 0, gridy : 0, anchor : GridBagConstraints.LINE_START, weightx: 100))
label(text : "Update trust lists every ", constraints : gbc(gridx:0, gridy:2)) allowUntrustedCheckbox = checkBox(selected : bind {model.onlyTrusted}, constraints : gbc(gridx: 1, gridy : 0, anchor : GridBagConstraints.LINE_END))
trustListIntervalField = textField(text : bind {model.trustListInterval}, constraints:gbc(gridx:1, gridy:2)) label(text : "Search extra hop", constraints : gbc(gridx:0, gridy:1, anchor : GridBagConstraints.LINE_START, weightx : 100))
label(text : "hours", constraints : gbc(gridx: 2, gridy:2)) searchExtraHopCheckbox = checkBox(selected : bind {model.searchExtraHop}, constraints : gbc(gridx: 1, gridy : 1, anchor : GridBagConstraints.LINE_END))
label(text : "Allow others to view my trust list", constraints : gbc(gridx: 0, gridy : 2, anchor : GridBagConstraints.LINE_START, weightx : 100))
allowTrustListsCheckbox = checkBox(selected : bind {model.trustLists}, constraints : gbc(gridx: 1, gridy : 2, anchor : GridBagConstraints.LINE_END))
label(text : "Update trust lists every (hours)", constraints : gbc(gridx:0, gridy:3, anchor : GridBagConstraints.LINE_START, weightx : 100))
trustListIntervalField = textField(text : bind {model.trustListInterval}, constraints:gbc(gridx:1, gridy:3, anchor : GridBagConstraints.LINE_END))
}
panel(constraints : gbc(gridx: 0, gridy : 1, weighty: 100))
} }

View File

@@ -18,10 +18,13 @@ import javax.swing.SwingConstants
import javax.swing.table.DefaultTableCellRenderer import javax.swing.table.DefaultTableCellRenderer
import com.muwire.core.Persona import com.muwire.core.Persona
import com.muwire.core.search.UIResultEvent
import com.muwire.core.util.DataUtil import com.muwire.core.util.DataUtil
import java.awt.BorderLayout import java.awt.BorderLayout
import java.awt.Color import java.awt.Color
import java.awt.FlowLayout
import java.awt.GridBagConstraints
import java.awt.Toolkit import java.awt.Toolkit
import java.awt.datatransfer.StringSelection import java.awt.datatransfer.StringSelection
import java.awt.event.MouseAdapter import java.awt.event.MouseAdapter
@@ -43,11 +46,13 @@ class SearchTabView {
def lastSendersSortEvent def lastSendersSortEvent
def resultsTable def resultsTable
def lastSortEvent def lastSortEvent
def sequentialDownloadCheckbox
void initUI() { void initUI() {
builder.with { builder.with {
def resultsTable def resultsTable
def sendersTable def sendersTable
def sequentialDownloadCheckbox
def pane = panel { def pane = panel {
gridLayout(rows :1, cols : 1) gridLayout(rows :1, cols : 1)
splitPane(orientation: JSplitPane.VERTICAL_SPLIT, continuousLayout : true, dividerLocation: 300 ) { splitPane(orientation: JSplitPane.VERTICAL_SPLIT, continuousLayout : true, dividerLocation: 300 ) {
@@ -58,6 +63,7 @@ class SearchTabView {
tableModel(list : model.senders) { tableModel(list : model.senders) {
closureColumn(header : "Sender", preferredWidth : 500, type: String, read : {row -> row.getHumanReadableName()}) 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 : "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 -> closureColumn(header : "Trust", preferredWidth : 50, type: String, read : { row ->
model.core.trustService.getLevel(row.destination).toString() model.core.trustService.getLevel(row.destination).toString()
}) })
@@ -65,8 +71,15 @@ class SearchTabView {
} }
} }
panel(constraints : BorderLayout.SOUTH) { panel(constraints : BorderLayout.SOUTH) {
button(text : "Trust", enabled: bind {model.trustButtonsEnabled }, trustAction) gridLayout(rows: 1, cols : 2)
button(text : "Distrust", enabled : bind {model.trustButtonsEnabled}, distrustAction) panel (border : etchedBorder()){
button(text : "Browse Host", enabled : bind {model.browseActionEnabled}, browseAction)
}
panel (border : etchedBorder()){
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 { panel {
@@ -78,11 +91,23 @@ class SearchTabView {
closureColumn(header: "Size", preferredWidth: 20, type: Long, read : {row -> row.size}) 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: "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: "Possible Sources", preferredWidth : 50, type : Integer, read : {row -> model.sourcesBucket[row.infohash].size()})
closureColumn(header: "Comments", preferredWidth: 20, type: Boolean, read : {row -> row.comment != null})
} }
} }
} }
panel(constraints : BorderLayout.SOUTH) { panel(constraints : BorderLayout.SOUTH) {
button(text : "Download", enabled : bind {model.downloadActionEnabled}, downloadAction) gridLayout(rows: 1, cols: 3)
panel()
panel {
button(text : "Download", enabled : bind {model.downloadActionEnabled}, downloadAction)
button(text : "View Comment", enabled : bind {model.viewCommentActionEnabled}, showCommentAction)
}
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")
}
} }
} }
} }
@@ -94,18 +119,26 @@ class SearchTabView {
this.resultsTable = resultsTable this.resultsTable = resultsTable
this.sendersTable = sendersTable this.sendersTable = sendersTable
this.sequentialDownloadCheckbox = sequentialDownloadCheckbox
def selectionModel = resultsTable.getSelectionModel() def selectionModel = resultsTable.getSelectionModel()
selectionModel.setSelectionMode(ListSelectionModel.SINGLE_SELECTION) selectionModel.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION)
selectionModel.addListSelectionListener( { selectionModel.addListSelectionListener( {
int row = resultsTable.getSelectedRow() int[] rows = resultsTable.getSelectedRows()
if (row < 0) { if (rows.length == 0) {
model.downloadActionEnabled = false model.downloadActionEnabled = false
return return
} }
if (lastSortEvent != null) if (lastSortEvent != null) {
row = resultsTable.rowSorter.convertRowIndexToModel(row) for (int i = 0; i < rows.length; i ++) {
model.downloadActionEnabled = mvcGroup.parentGroup.model.canDownload(model.results[row].infohash) rows[i] = resultsTable.rowSorter.convertRowIndexToModel(rows[i])
}
}
boolean downloadActionEnabled = true
rows.each {
downloadActionEnabled &= mvcGroup.parentGroup.model.canDownload(model.results[it].infohash)
}
model.downloadActionEnabled = downloadActionEnabled
}) })
} }
} }
@@ -158,6 +191,16 @@ class SearchTabView {
} }
}) })
resultsTable.getSelectionModel().addListSelectionListener({
def result = getSelectedResult()
if (result == null) {
model.viewCommentActionEnabled = false
return
} else {
model.viewCommentActionEnabled = result.comment != null
}
})
// senders table // senders table
sendersTable.setDefaultRenderer(Integer.class, centerRenderer) sendersTable.setDefaultRenderer(Integer.class, centerRenderer)
sendersTable.rowSorter.addRowSorterListener({evt -> lastSendersSortEvent = evt}) sendersTable.rowSorter.addRowSorterListener({evt -> lastSendersSortEvent = evt})
@@ -168,12 +211,14 @@ class SearchTabView {
int row = selectedSenderRow() int row = selectedSenderRow()
if (row < 0) { if (row < 0) {
model.trustButtonsEnabled = false model.trustButtonsEnabled = false
model.browseActionEnabled = false
return return
} else { } else {
Persona sender = model.senders[row]
model.browseActionEnabled = model.sendersBucket[sender].first().browse
model.trustButtonsEnabled = true model.trustButtonsEnabled = true
model.results.clear() model.results.clear()
Persona p = model.senders[row] model.results.addAll(model.sendersBucket[sender])
model.results.addAll(model.sendersBucket[p])
resultsTable.model.fireTableDataChanged() resultsTable.model.fireTableDataChanged()
} }
}) })
@@ -190,29 +235,62 @@ class SearchTabView {
def showPopupMenu(MouseEvent e) { def showPopupMenu(MouseEvent e) {
JPopupMenu menu = new JPopupMenu() JPopupMenu menu = new JPopupMenu()
boolean showMenu = false
if (model.downloadActionEnabled) { if (model.downloadActionEnabled) {
JMenuItem download = new JMenuItem("Download") JMenuItem download = new JMenuItem("Download")
download.addActionListener({mvcGroup.controller.download()}) download.addActionListener({mvcGroup.controller.download()})
menu.add(download) menu.add(download)
showMenu = true
} }
JMenuItem copyHashToClipboard = new JMenuItem("Copy hash to clipboard") if (resultsTable.getSelectedRows().length == 1) {
copyHashToClipboard.addActionListener({mvcGroup.view.copyHashToClipboard()}) JMenuItem copyHashToClipboard = new JMenuItem("Copy hash to clipboard")
menu.add(copyHashToClipboard) copyHashToClipboard.addActionListener({mvcGroup.view.copyHashToClipboard()})
menu.show(e.getComponent(), e.getX(), e.getY()) 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
if (model.viewCommentActionEnabled) {
JMenuItem showComment = new JMenuItem("View Comment")
showComment.addActionListener({mvcGroup.controller.showComment()})
menu.add(showComment)
}
}
if (showMenu)
menu.show(e.getComponent(), e.getX(), e.getY())
}
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)
model.results[selected]
} }
def copyHashToClipboard() { def copyHashToClipboard() {
int selected = resultsTable.getSelectedRow() def result = getSelectedResult()
if (selected < 0) if (result == null)
return return
if (lastSortEvent != null) String hash = Base64.encode(result.infohash.getRoot())
selected = resultsTable.rowSorter.convertRowIndexToModel(selected)
String hash = Base64.encode(model.results[selected].infohash.getRoot())
StringSelection selection = new StringSelection(hash) StringSelection selection = new StringSelection(hash)
def clipboard = Toolkit.getDefaultToolkit().getSystemClipboard() def clipboard = Toolkit.getDefaultToolkit().getSystemClipboard()
clipboard.setContents(selection, null) 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)
}
int selectedSenderRow() { int selectedSenderRow() {
int row = sendersTable.getSelectedRow() int row = sendersTable.getSelectedRow()
if (row < 0) if (row < 0)

View 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()
}
}

View File

@@ -0,0 +1,18 @@
package com.muwire.gui
class InterimTreeNode {
private final File file
InterimTreeNode(File file) {
this.file = file
}
public boolean equals(Object o) {
if (!(o instanceof InterimTreeNode))
return false
file == o.file
}
public String toString() {
file.getName()
}
}

View File

@@ -0,0 +1,44 @@
package com.muwire.gui
import java.awt.Component
import javax.swing.ImageIcon
import javax.swing.JTree
import javax.swing.tree.DefaultTreeCellRenderer
import com.muwire.core.SharedFile
import net.i2p.data.DataHelper
class SharedTreeRenderer extends DefaultTreeCellRenderer {
private final ImageIcon commentIcon
SharedTreeRenderer() {
commentIcon = new ImageIcon((URL) SharedTreeRenderer.class.getResource("/comment.png"))
}
public Component getTreeCellRendererComponent(JTree tree, Object value,
boolean sel, boolean expanded, boolean leaf, int row, boolean hasFocus) {
def userObject = value.getUserObject()
def defaultRenderer = super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus)
if (userObject instanceof InterimTreeNode || userObject == null)
return defaultRenderer
SharedFile sf = (SharedFile) userObject
String name = sf.getFile().getName()
long length = sf.getCachedLength()
String formatted = DataHelper.formatSize2Decimal(length, false)+"B"
setText("$name ($formatted)")
setEnabled(true)
if (sf.comment != null) {
setIcon(commentIcon)
}
this
}
}

View File

@@ -5,11 +5,13 @@ class UISettings {
String lnf String lnf
boolean showMonitor boolean showMonitor
String font String font
boolean autoFontSize
int fontSize
boolean clearCancelledDownloads boolean clearCancelledDownloads
boolean clearFinishedDownloads boolean clearFinishedDownloads
boolean excludeLocalResult boolean excludeLocalResult
boolean showSearchHashes boolean showSearchHashes
UISettings(Properties props) { UISettings(Properties props) {
lnf = props.getProperty("lnf", "system") lnf = props.getProperty("lnf", "system")
showMonitor = Boolean.parseBoolean(props.getProperty("showMonitor", "false")) showMonitor = Boolean.parseBoolean(props.getProperty("showMonitor", "false"))
@@ -18,6 +20,8 @@ class UISettings {
clearFinishedDownloads = Boolean.parseBoolean(props.getProperty("clearFinishedDownloads","false")) clearFinishedDownloads = Boolean.parseBoolean(props.getProperty("clearFinishedDownloads","false"))
excludeLocalResult = Boolean.parseBoolean(props.getProperty("excludeLocalResult","true")) excludeLocalResult = Boolean.parseBoolean(props.getProperty("excludeLocalResult","true"))
showSearchHashes = Boolean.parseBoolean(props.getProperty("showSearchHashes","true")) showSearchHashes = Boolean.parseBoolean(props.getProperty("showSearchHashes","true"))
autoFontSize = Boolean.parseBoolean(props.getProperty("autoFontSize","false"))
fontSize = Integer.parseInt(props.getProperty("fontSize","12"))
} }
void write(OutputStream out) throws IOException { void write(OutputStream out) throws IOException {
@@ -28,6 +32,8 @@ class UISettings {
props.setProperty("clearFinishedDownloads", String.valueOf(clearFinishedDownloads)) props.setProperty("clearFinishedDownloads", String.valueOf(clearFinishedDownloads))
props.setProperty("excludeLocalResult", String.valueOf(excludeLocalResult)) props.setProperty("excludeLocalResult", String.valueOf(excludeLocalResult))
props.setProperty("showSearchHashes", String.valueOf(showSearchHashes)) props.setProperty("showSearchHashes", String.valueOf(showSearchHashes))
props.setProperty("autoFontSize", String.valueOf(autoFontSize))
props.setProperty("fontSize", String.valueOf(fontSize))
if (font != null) if (font != null)
props.setProperty("font", font) props.setProperty("font", font)

View File

@@ -1,21 +0,0 @@
package com.muwire.gui
import griffon.core.test.GriffonUnitRule
import griffon.core.test.TestFor
import org.junit.Rule
import org.junit.Test
import static org.junit.Assert.fail
@TestFor(ContentPanelController)
class ContentPanelControllerTest {
private ContentPanelController controller
@Rule
public final GriffonUnitRule griffon = new GriffonUnitRule()
@Test
void smokeTest() {
fail('Not yet implemented!')
}
}

View File

@@ -1,21 +0,0 @@
package com.muwire.gui
import griffon.core.test.GriffonUnitRule
import griffon.core.test.TestFor
import org.junit.Rule
import org.junit.Test
import static org.junit.Assert.fail
@TestFor(EventListController)
class EventListControllerTest {
private EventListController controller
@Rule
public final GriffonUnitRule griffon = new GriffonUnitRule()
@Test
void smokeTest() {
fail('Not yet implemented!')
}
}

View File

@@ -1,21 +0,0 @@
package com.muwire.gui
import griffon.core.test.GriffonUnitRule
import griffon.core.test.TestFor
import org.junit.Rule
import org.junit.Test
import static org.junit.Assert.fail
@TestFor(I2PStatusController)
class I2PStatusControllerTest {
private I2PStatusController controller
@Rule
public final GriffonUnitRule griffon = new GriffonUnitRule()
@Test
void smokeTest() {
fail('Not yet implemented!')
}
}

View File

@@ -1,21 +0,0 @@
package com.muwire.gui
import griffon.core.test.GriffonUnitRule
import griffon.core.test.TestFor
import org.junit.Rule
import org.junit.Test
import static org.junit.Assert.fail
@TestFor(MainFrameController)
class MainFrameControllerTest {
private MainFrameController controller
@Rule
public final GriffonUnitRule griffon = new GriffonUnitRule()
@Test
void smokeTest() {
fail('Not yet implemented!')
}
}

View File

@@ -1,21 +0,0 @@
package com.muwire.gui
import griffon.core.test.GriffonUnitRule
import griffon.core.test.TestFor
import org.junit.Rule
import org.junit.Test
import static org.junit.Assert.fail
@TestFor(MuWireStatusController)
class MuWireStatusControllerTest {
private MuWireStatusController controller
@Rule
public final GriffonUnitRule griffon = new GriffonUnitRule()
@Test
void smokeTest() {
fail('Not yet implemented!')
}
}

View File

@@ -1,21 +0,0 @@
package com.muwire.gui
import griffon.core.test.GriffonUnitRule
import griffon.core.test.TestFor
import org.junit.Rule
import org.junit.Test
import static org.junit.Assert.fail
@TestFor(OptionsController)
class OptionsControllerTest {
private OptionsController controller
@Rule
public final GriffonUnitRule griffon = new GriffonUnitRule()
@Test
void smokeTest() {
fail('Not yet implemented!')
}
}

View File

@@ -1,21 +0,0 @@
package com.muwire.gui
import griffon.core.test.GriffonUnitRule
import griffon.core.test.TestFor
import org.junit.Rule
import org.junit.Test
import static org.junit.Assert.fail
@TestFor(SearchTabController)
class SearchTabControllerTest {
private SearchTabController controller
@Rule
public final GriffonUnitRule griffon = new GriffonUnitRule()
@Test
void smokeTest() {
fail('Not yet implemented!')
}
}

View File

@@ -1,21 +0,0 @@
package com.muwire.gui
import griffon.core.test.GriffonUnitRule
import griffon.core.test.TestFor
import org.junit.Rule
import org.junit.Test
import static org.junit.Assert.fail
@TestFor(TrustListController)
class TrustListControllerTest {
private TrustListController controller
@Rule
public final GriffonUnitRule griffon = new GriffonUnitRule()
@Test
void smokeTest() {
fail('Not yet implemented!')
}
}

View File

@@ -1,3 +1,8 @@
apply plugin : 'application' apply plugin : 'application'
mainClassName = 'com.muwire.hostcache.HostCache' mainClassName = 'com.muwire.hostcache.HostCache'
applicationDefaultJvmArgs = ['-Djava.util.logging.config.file=logging.properties'] applicationDefaultJvmArgs = ['-Djava.util.logging.config.file=logging.properties']
dependencies {
testCompile 'org.junit.jupiter:junit-jupiter-api:5.4.2'
testCompile 'junit:junit:4.12'
}