Compare commits

..

29 Commits

Author SHA1 Message Date
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
41 changed files with 1121 additions and 287 deletions

View File

@@ -35,7 +35,7 @@ class Cli {
Core core Core core
try { try {
core = new Core(props, home, "0.4.16") core = new Core(props, home, "0.5.0")
} 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.16") core = new Core(props, home, "0.5.0")
} 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

@@ -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
@@ -215,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)
@@ -223,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")
@@ -251,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)
@@ -277,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)
@@ -298,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() {
@@ -362,7 +375,7 @@ public class Core {
} }
} }
Core core = new Core(props, home, "0.4.16") Core core = new Core(props, home, "0.5.0")
core.startServices() core.startServices()
// ... at the end, sleep or execute script // ... at the end, sleep or execute script

View File

@@ -6,6 +6,7 @@ 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 {
@@ -23,6 +24,9 @@ class MuWireSettings {
File downloadLocation File downloadLocation
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, hostRejectInterval int hostClearInterval, hostHopelessInterval, hostRejectInterval
@@ -51,6 +55,7 @@ class MuWireSettings {
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"))
@@ -59,6 +64,8 @@ class MuWireSettings {
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"))
watchedDirectories = readEncodedSet(props, "watchedDirectories") watchedDirectories = readEncodedSet(props, "watchedDirectories")
watchedKeywords = readEncodedSet(props, "watchedKeywords") watchedKeywords = readEncodedSet(props, "watchedKeywords")
@@ -89,6 +96,7 @@ class MuWireSettings {
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))
@@ -97,6 +105,8 @@ class MuWireSettings {
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))
writeEncodedSet(watchedDirectories, "watchedDirectories", props) writeEncodedSet(watchedDirectories, "watchedDirectories", props)
writeEncodedSet(watchedKeywords, "watchedKeywords", props) writeEncodedSet(watchedKeywords, "watchedKeywords", props)
@@ -113,7 +123,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

@@ -132,6 +132,7 @@ 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
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 +210,15 @@ 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
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)
QueryEvent event = new QueryEvent ( searchEvent : searchEvent, QueryEvent event = new QueryEvent ( searchEvent : searchEvent,
replyTo : replyTo, replyTo : replyTo,
originator : originator, originator : originator,

View File

@@ -10,6 +10,7 @@ import java.util.zip.InflaterInputStream
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 +18,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
@@ -37,6 +39,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 +50,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 +58,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
@@ -129,6 +133,9 @@ class ConnectionAcceptor {
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")
} }
@@ -246,44 +253,86 @@ class ConnectionAcceptor {
e.close() 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(os)
JsonOutput jsonOutput = new JsonOutput()
sharedFiles.each {
def obj = ResultsSender.sharedFileToObj(it, false)
def json = jsonOutput.toJson(obj)
dos.writeShort((short)json.length())
dos.write(json.getBytes(StandardCharsets.US_ASCII))
}
dos.flush()
} finally {
e.close()
}
}
private void processTRUST(Endpoint e) { 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

@@ -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,6 +50,10 @@ 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) Thread.currentThread().setPriority(Thread.MIN_PRIORITY)
@@ -127,19 +137,21 @@ class PersisterService extends Service {
} }
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) {

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

@@ -0,0 +1,86 @@
package com.muwire.core.search
import com.muwire.core.Constants
import com.muwire.core.EventBus
import com.muwire.core.connection.Endpoint
import com.muwire.core.connection.I2PConnector
import com.muwire.core.util.DataUtil
import groovy.json.JsonSlurper
import groovy.util.logging.Log
import java.nio.charset.StandardCharsets
import java.util.concurrent.Executor
import java.util.concurrent.Executors
import java.util.logging.Level
@Log
class BrowseManager {
private final I2PConnector connector
private final EventBus eventBus
private final Executor browserThread = Executors.newSingleThreadExecutor()
BrowseManager(I2PConnector connector, EventBus eventBus) {
this.connector = connector
this.eventBus = eventBus
}
void onUIBrowseEvent(UIBrowseEvent e) {
browserThread.execute({
Endpoint endpoint = null
try {
eventBus.publish(new BrowseStatusEvent(status : BrowseStatus.CONNECTING))
endpoint = connector.connect(e.host.destination)
OutputStream os = endpoint.getOutputStream()
os.write("BROWSE\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
InputStream is = endpoint.getInputStream()
String code = DataUtil.readTillRN(is)
if (!code.startsWith("200"))
throw new IOException("Invalid code")
// parse all headers
Map<String,String> headers = new HashMap<>()
String header
while((header = DataUtil.readTillRN(is)) != "" && headers.size() < Constants.MAX_HEADERS) {
int colon = header.indexOf(':')
if (colon == -1 || colon == header.length() - 1)
throw new IOException("invalid header $header")
String key = header.substring(0, colon)
String value = header.substring(colon + 1)
headers[key] = value.trim()
}
if (!headers.containsKey("Count"))
throw new IOException("No count header")
int results = Integer.parseInt(headers['Count'])
// at this stage, start pulling the results
eventBus.publish(new BrowseStatusEvent(status : BrowseStatus.FETCHING))
JsonSlurper slurper = new JsonSlurper()
DataInputStream dis = new DataInputStream(is)
UUID uuid = UUID.randomUUID()
for (int i = 0; i < results; i++) {
int size = dis.readUnsignedShort()
byte [] tmp = new byte[size]
dis.readFully(tmp)
def json = slurper.parse(tmp)
UIResultEvent result = ResultsParser.parse(e.host, uuid, json)
eventBus.publish(result)
}
eventBus.publish(new BrowseStatusEvent(status : BrowseStatus.FINISHED))
} catch (Exception bad) {
log.log(Level.WARNING, "browse failed", bad)
eventBus.publish(new BrowseStatusEvent(status : BrowseStatus.FAILED))
} finally {
endpoint?.close()
}
} as Runnable)
}
}

View File

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

View File

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

View File

@@ -94,6 +94,10 @@ class ResultsParser {
String comment = null String comment = null
if (json.comment != null) if (json.comment != null)
comment = DataUtil.readi18nString(Base64.decode(json.comment)) comment = DataUtil.readi18nString(Base64.decode(json.comment))
boolean browse = false
if (json.browse != null)
browse = true
return new UIResultEvent( sender : p, return new UIResultEvent( sender : p,
name : name, name : name,
@@ -102,6 +106,7 @@ class ResultsParser {
pieceSize : pieceSize, pieceSize : pieceSize,
sources : sources, sources : sources,
comment : comment, 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
@@ -17,6 +18,7 @@ import java.util.stream.Collectors
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,11 +44,13 @@ 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) {
@@ -60,13 +64,18 @@ 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) eventBus.publish(uiResultEvent)
} }
@@ -85,7 +94,6 @@ class ResultsSender {
@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 { try {
@@ -95,36 +103,7 @@ class ResultsSender {
me.write(os) me.write(os)
os.writeShort((short)results.length) os.writeShort((short)results.length)
results.each { results.each {
byte [] name = it.getFile().getName().getBytes(StandardCharsets.UTF_8) def obj = sharedFileToObj(it, settings.browseFiles)
def baos = new ByteArrayOutputStream()
def daos = new DataOutputStream(baos)
daos.writeShort((short) name.length)
daos.write(name)
daos.flush()
String encodedName = Base64.encode(baos.toByteArray())
def obj = [:]
obj.type = "Result"
obj.version = oobInfohash ? 2 : 1
obj.name = encodedName
obj.infohash = Base64.encode(it.getInfoHash().getRoot())
obj.size = it.getFile().length()
obj.pieceSize = it.getPieceSize()
if (!oobInfohash) {
byte [] hashList = it.getInfoHash().getHashList()
def hashListB64 = []
for (int i = 0; i < hashList.length / InfoHash.SIZE; i++) {
System.arraycopy(hashList, InfoHash.SIZE * i, tmp, 0, InfoHash.SIZE)
hashListB64 << Base64.encode(tmp)
}
obj.hashList = hashListB64
}
if (it instanceof DownloadedFile)
obj.sources = it.sources.stream().map({dest -> dest.toBase64()}).collect(Collectors.toSet())
if (it.getComment() != null)
obj.comment = it.getComment()
def json = jsonOutput.toJson(obj) def json = jsonOutput.toJson(obj)
os.writeShort((short)json.length()) os.writeShort((short)json.length())
os.write(json.getBytes(StandardCharsets.US_ASCII)) os.write(json.getBytes(StandardCharsets.US_ASCII))
@@ -138,4 +117,30 @@ class ResultsSender {
} }
} }
} }
public static def sharedFileToObj(SharedFile sf, boolean browseFiles) {
byte [] name = sf.getFile().getName().getBytes(StandardCharsets.UTF_8)
def baos = new ByteArrayOutputStream()
def daos = new DataOutputStream(baos)
daos.writeShort((short) name.length)
daos.write(name)
daos.flush()
String encodedName = Base64.encode(baos.toByteArray())
def obj = [:]
obj.type = "Result"
obj.version = 2
obj.name = encodedName
obj.infohash = Base64.encode(sf.getInfoHash().getRoot())
obj.size = sf.getCachedLength()
obj.pieceSize = sf.getPieceSize()
if (sf instanceof DownloadedFile)
obj.sources = sf.sources.stream().map({dest -> dest.toBase64()}).collect(Collectors.toSet())
if (sf.getComment() != null)
obj.comment = sf.getComment()
obj.browse = browseFiles
obj
}
} }

View File

@@ -9,11 +9,12 @@ class SearchEvent extends Event {
byte [] searchHash byte [] searchHash
UUID uuid UUID uuid
boolean oobInfohash boolean oobInfohash
boolean searchComments
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"
} }
} }

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

@@ -15,6 +15,7 @@ class UIResultEvent extends Event {
InfoHash infohash InfoHash infohash
int pieceSize int pieceSize
String comment String comment
boolean browse
@Override @Override
public String toString() { public String toString() {

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

@@ -1,5 +1,5 @@
group = com.muwire group = com.muwire
version = 0.4.16 version = 0.5.0
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

View File

@@ -56,4 +56,9 @@ mvcGroups {
view = 'com.muwire.gui.AddCommentView' view = 'com.muwire.gui.AddCommentView'
controller = 'com.muwire.gui.AddCommentController' controller = 'com.muwire.gui.AddCommentController'
} }
'browse' {
model = 'com.muwire.gui.BrowseModel'
view = 'com.muwire.gui.BrowseView'
controller = 'com.muwire.gui.BrowseController'
}
} }

View File

@@ -8,6 +8,8 @@ import net.i2p.data.Base64
import javax.annotation.Nonnull import javax.annotation.Nonnull
import com.muwire.core.Core
import com.muwire.core.files.UICommentEvent
import com.muwire.core.util.DataUtil import com.muwire.core.util.DataUtil
@ArtifactProviderFor(GriffonController) @ArtifactProviderFor(GriffonController)
@@ -17,6 +19,8 @@ class AddCommentController {
@MVCMember @Nonnull @MVCMember @Nonnull
AddCommentView view AddCommentView view
Core core
@ControllerAction @ControllerAction
void save() { void save() {
String comment = view.textarea.getText() String comment = view.textarea.getText()
@@ -25,9 +29,11 @@ class AddCommentController {
else else
comment = Base64.encode(DataUtil.encodei18nString(comment)) comment = Base64.encode(DataUtil.encodei18nString(comment))
model.selectedFiles.each { model.selectedFiles.each {
def event = new UICommentEvent(sharedFile : it, oldComment : it.getComment())
it.setComment(comment) it.setComment(comment)
core.eventBus.publish(event)
} }
mvcGroup.parentGroup.view.builder.getVariable("shared-files-table").model.fireTableDataChanged() mvcGroup.parentGroup.view.refreshSharedFiles()
cancel() cancel()
} }

View File

@@ -0,0 +1,95 @@
package com.muwire.gui
import griffon.core.artifact.GriffonController
import griffon.core.controller.ControllerAction
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import net.i2p.data.Base64
import javax.annotation.Nonnull
import com.muwire.core.EventBus
import com.muwire.core.download.UIDownloadEvent
import com.muwire.core.search.BrowseStatusEvent
import com.muwire.core.search.UIBrowseEvent
import com.muwire.core.search.UIResultEvent
@ArtifactProviderFor(GriffonController)
class BrowseController {
@MVCMember @Nonnull
BrowseModel model
@MVCMember @Nonnull
BrowseView view
EventBus eventBus
void register() {
eventBus.register(BrowseStatusEvent.class, this)
eventBus.register(UIResultEvent.class, this)
eventBus.publish(new UIBrowseEvent(host : model.host))
}
void mvcGroupDestroy() {
eventBus.unregister(BrowseStatusEvent.class, this)
eventBus.unregister(UIResultEvent.class, this)
}
void onBrowseStatusEvent(BrowseStatusEvent e) {
runInsideUIAsync {
model.status = e.status
}
}
void onUIResultEvent(UIResultEvent e) {
runInsideUIAsync {
model.results << e
view.resultsTable.model.fireTableDataChanged()
}
}
@ControllerAction
void dismiss() {
view.dialog.setVisible(false)
mvcGroup.destroy()
}
@ControllerAction
void download() {
def selectedResults = view.selectedResults()
if (selectedResults == null || selectedResults.isEmpty())
return
selectedResults.removeAll {
!mvcGroup.parentGroup.parentGroup.model.canDownload(it.infohash)
}
selectedResults.each { result ->
def file = new File(application.context.get("muwire-settings").downloadLocation, result.name)
eventBus.publish(new UIDownloadEvent(
result : [result],
sources : [model.host.destination],
target : file,
sequential : mvcGroup.parentGroup.view.sequentialDownloadCheckbox.model.isSelected()
))
}
mvcGroup.parentGroup.parentGroup.view.showDownloadsWindow.call()
dismiss()
}
@ControllerAction
void viewComment() {
def selectedResults = view.selectedResults()
if (selectedResults == null || selectedResults.size() != 1)
return
def result = selectedResults[0]
if (result.comment == null)
return
String groupId = Base64.encode(result.infohash.getRoot())
Map<String,Object> params = new HashMap<>()
params['result'] = result
mvcGroup.createMVCGroup("show-comment", groupId, params)
}
}

View File

@@ -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
@@ -84,7 +85,8 @@ class MainFrameController {
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)
} }
boolean firstHop = core.muOptions.allowUntrusted || core.muOptions.searchExtraHop boolean firstHop = core.muOptions.allowUntrusted || core.muOptions.searchExtraHop
core.eventBus.publish(new QueryEvent(searchEvent : searchEvent, firstHop : firstHop, core.eventBus.publish(new QueryEvent(searchEvent : searchEvent, firstHop : firstHop,
@@ -276,6 +278,7 @@ class MainFrameController {
sf.each { sf.each {
core.eventBus.publish(new FileUnsharedEvent(unsharedFile : it)) core.eventBus.publish(new FileUnsharedEvent(unsharedFile : it))
} }
core.eventBus.publish(new UIPersistFilesEvent())
} }
@ControllerAction @ControllerAction
@@ -286,21 +289,10 @@ class MainFrameController {
Map<String, Object> params = new HashMap<>() Map<String, Object> params = new HashMap<>()
params['selectedFiles'] = selectedFiles params['selectedFiles'] = selectedFiles
params['core'] = core
mvcGroup.createMVCGroup("add-comment", "Add Comment", params) mvcGroup.createMVCGroup("add-comment", "Add Comment", params)
} }
void stopWatchingDirectory() {
String directory = mvcGroup.view.getSelectedWatchedDirectory()
if (directory == null)
return
core.muOptions.watchedDirectories.remove(directory)
saveMuWireSettings()
core.eventBus.publish(new DirectoryUnsharedEvent(directory : new File(directory)))
model.watched.remove(directory)
builder.getVariable("watched-directories-table").model.fireTableDataChanged()
}
void saveMuWireSettings() { void saveMuWireSettings() {
File f = new File(core.home, "MuWire.properties") File f = new File(core.home, "MuWire.properties")
f.withOutputStream { f.withOutputStream {

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,7 +82,15 @@ 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
String downloadLocation = model.downloadLocation String downloadLocation = model.downloadLocation
settings.downloadLocation = new File(downloadLocation) settings.downloadLocation = new File(downloadLocation)
@@ -123,10 +135,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
@@ -140,10 +151,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)
@@ -167,5 +174,16 @@ class OptionsController {
int rv = chooser.showOpenDialog(null) int rv = chooser.showOpenDialog(null)
if (rv == JFileChooser.APPROVE_OPTION) if (rv == JFileChooser.APPROVE_OPTION)
model.downloadLocation = chooser.getSelectedFile().getAbsolutePath() model.downloadLocation = chooser.getSelectedFile().getAbsolutePath()
}
@ControllerAction
void automaticFontAction() {
model.automaticFontSize = true
model.customFontSize = 12
}
@ControllerAction
void customFontAction() {
model.automaticFontSize = false
} }
} }

View File

@@ -7,6 +7,7 @@ import griffon.metadata.ArtifactProviderFor
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.search.UIResultEvent
import com.muwire.core.trust.TrustEvent import com.muwire.core.trust.TrustEvent
@@ -85,4 +86,19 @@ class SearchTabController {
def sender = model.senders[row] def sender = model.senders[row]
core.eventBus.publish( new TrustEvent(persona : sender, level : TrustLevel.NEUTRAL)) 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)
}
} }

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 logging")
def names = LogManager.getLogManager().getLoggerNames()
while(names.hasMoreElements()) {
def name = names.nextElement()
LogManager.getLogManager().getLogger(name).setLevel(Level.OFF)
}
}
log.info "Loading home dir" 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

@@ -0,0 +1,19 @@
package com.muwire.gui
import com.muwire.core.Persona
import griffon.core.artifact.GriffonModel
import griffon.transform.Observable
import griffon.metadata.ArtifactProviderFor
import com.muwire.core.search.BrowseStatus
@ArtifactProviderFor(GriffonModel)
class BrowseModel {
Persona host
@Observable BrowseStatus status
@Observable boolean downloadActionEnabled
@Observable boolean viewCommentActionEnabled
def results = []
}

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 = []
@@ -122,6 +134,10 @@ class MainFrameModel {
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({
@@ -233,9 +249,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))
@@ -303,7 +317,6 @@ class MainFrameModel {
void onFileHashingEvent(FileHashingEvent e) { void onFileHashingEvent(FileHashingEvent e) {
runInsideUIAsync { runInsideUIAsync {
loadedFiles = shared.size()
hashingFile = e.hashingFile hashingFile = e.hashingFile
} }
} }
@@ -319,6 +332,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()
} }
} }
@@ -328,6 +343,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()
} }
} }
@@ -335,8 +352,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()
} }
} }
@@ -479,8 +514,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

@@ -13,7 +13,10 @@ 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 boolean searchComments
@Observable boolean browseFiles
// i2p options // i2p options
@Observable String inboundLength @Observable String inboundLength
@@ -27,6 +30,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
@@ -49,7 +54,10 @@ 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()
searchComments = settings.searchComments
browseFiles = settings.browseFiles
Core core = application.context.get("core") Core core = application.context.get("core")
inboundLength = core.i2pOptions["inbound.length"] inboundLength = core.i2pOptions["inbound.length"]
@@ -63,6 +71,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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 598 B

View File

@@ -0,0 +1,132 @@
package com.muwire.gui
import griffon.core.artifact.GriffonView
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import javax.swing.JDialog
import javax.swing.JLabel
import javax.swing.ListSelectionModel
import javax.swing.SwingConstants
import javax.swing.table.DefaultTableCellRenderer
import com.muwire.core.search.UIResultEvent
import java.awt.BorderLayout
import java.awt.event.WindowAdapter
import java.awt.event.WindowEvent
import javax.annotation.Nonnull
@ArtifactProviderFor(GriffonView)
class BrowseView {
@MVCMember @Nonnull
FactoryBuilderSupport builder
@MVCMember @Nonnull
BrowseModel model
@MVCMember @Nonnull
BrowseController controller
def mainFrame
def dialog
def p
def resultsTable
def lastSortEvent
void initUI() {
mainFrame = application.windowManager.findWindow("main-frame")
dialog = new JDialog(mainFrame, model.host.getHumanReadableName(), true)
dialog.setResizable(true)
p = builder.panel {
borderLayout()
panel (constraints : BorderLayout.NORTH) {
label(text: "Status:")
label(text: bind {model.status.toString()})
}
scrollPane (constraints : BorderLayout.CENTER){
resultsTable = table(autoCreateRowSorter : true) {
tableModel(list : model.results) {
closureColumn(header: "Name", preferredWidth: 350, type: String, read : {row -> row.name.replace('<','_')})
closureColumn(header: "Size", preferredWidth: 20, type: Long, read : {row -> row.size})
closureColumn(header: "Comments", preferredWidth: 20, type: Boolean, read : {row -> row.comment != null})
}
}
}
panel (constraints : BorderLayout.SOUTH) {
button(text : "Download", enabled : bind {model.downloadActionEnabled}, downloadAction)
button(text : "View Comment", enabled : bind{model.viewCommentActionEnabled}, viewCommentAction)
button(text : "Dismiss", dismissAction)
}
}
def centerRenderer = new DefaultTableCellRenderer()
centerRenderer.setHorizontalAlignment(JLabel.CENTER)
resultsTable.setDefaultRenderer(Integer.class,centerRenderer)
resultsTable.columnModel.getColumn(1).setCellRenderer(new SizeRenderer())
resultsTable.rowSorter.addRowSorterListener({evt -> lastSortEvent = evt})
resultsTable.rowSorter.setSortsOnUpdates(true)
def selectionModel = resultsTable.getSelectionModel()
selectionModel.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION)
selectionModel.addListSelectionListener({
int[] rows = resultsTable.getSelectedRows()
if (rows.length == 0) {
model.downloadActionEnabled = false
model.viewCommentActionEnabled = false
return
}
if (lastSortEvent != null) {
for (int i = 0; i < rows.length; i ++) {
rows[i] = resultsTable.rowSorter.convertRowIndexToModel(rows[i])
}
}
boolean downloadActionEnabled = true
if (rows.length == 1 && model.results[rows[0]].comment != null)
model.viewCommentActionEnabled = true
else
model.viewCommentActionEnabled = false
rows.each {
downloadActionEnabled &= mvcGroup.parentGroup.parentGroup.model.canDownload(model.results[it].infohash)
}
model.downloadActionEnabled = downloadActionEnabled
})
}
void mvcGroupInit(Map<String,String> args) {
controller.register()
dialog.getContentPane().add(p)
dialog.setSize(700, 400)
dialog.setLocationRelativeTo(mainFrame)
dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE)
dialog.addWindowListener( new WindowAdapter() {
public void windowClosed(WindowEvent e) {
mvcGroup.destroy()
}
})
dialog.show()
}
def selectedResults() {
int [] rows = resultsTable.getSelectedRows()
if (rows.length == 0)
return null
if (lastSortEvent != null) {
for (int i = 0; i < rows.length; i ++) {
rows[i] = resultsTable.rowSorter.convertRowIndexToModel(rows[i])
}
}
List<UIResultEvent> rv = new ArrayList<>()
for (Integer i : rows)
rv << model.results[i]
rv
}
}

View File

@@ -16,11 +16,14 @@ 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.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
@@ -56,11 +59,12 @@ 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,
@@ -193,44 +197,46 @@ class MainFrameView {
}) })
} }
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() })
closureColumn(header : "Comments", preferredWidth : 100, type : Boolean, read : {it.getComment() != null})
}
} }
} }
} }
} }
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 {
button(text : "Share files", actionPerformed : shareFiles)
button(text : "Add Comment", enabled : bind {model.addCommentButtonEnabled}, addCommentAction)
} }
panel { panel {
gridLayout(rows : 1, cols : 2)
panel { panel {
label("Shared:") label("Shared:")
label(text : bind {model.loadedFiles.toString()}) label(text : bind {model.loadedFiles}, id : "shared-files-count")
}
panel {
button(text : "Add Comment", enabled : bind {model.addCommentButtonEnabled}, addCommentAction)
} }
} }
} }
@@ -411,10 +417,7 @@ class MainFrameView {
public boolean importData(TransferHandler.TransferSupport support) { public boolean importData(TransferHandler.TransferSupport support) {
def files = support.getTransferable().getTransferData(DataFlavor.javaFileListFlavor) def files = support.getTransferable().getTransferData(DataFlavor.javaFileListFlavor)
files.each { files.each {
if (it.isDirectory()) model.core.eventBus.publish(new FileSharedEvent(file : it))
watchDirectory(it)
else
model.core.eventBus.publish(new FileSharedEvent(file : it))
} }
showUploadsWindow.call() showUploadsWindow.call()
true true
@@ -489,13 +492,7 @@ class MainFrameView {
} }
}) })
// shared files table // shared files menu
def sharedFilesTable = builder.getVariable("shared-files-table")
sharedFilesTable.columnModel.getColumn(1).setCellRenderer(new SizeRenderer())
sharedFilesTable.rowSorter.addRowSorterListener({evt -> lastSharedSortEvent = evt})
sharedFilesTable.rowSorter.setSortsOnUpdates(true)
JPopupMenu sharedFilesMenu = new JPopupMenu() 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()})
@@ -506,7 +503,8 @@ class MainFrameView {
JMenuItem commentSelectedFiles = new JMenuItem("Comment selected files") JMenuItem commentSelectedFiles = new JMenuItem("Comment selected files")
commentSelectedFiles.addActionListener({mvcGroup.controller.addComment()}) commentSelectedFiles.addActionListener({mvcGroup.controller.addComment()})
sharedFilesMenu.add(commentSelectedFiles) sharedFilesMenu.add(commentSelectedFiles)
sharedFilesTable.addMouseListener(new MouseAdapter() {
def sharedFilesMouseListener = new MouseAdapter() {
@Override @Override
public void mouseReleased(MouseEvent e) { public void mouseReleased(MouseEvent e) {
if (e.isPopupTrigger()) if (e.isPopupTrigger())
@@ -517,7 +515,16 @@ class MainFrameView {
if (e.isPopupTrigger()) if (e.isPopupTrigger())
showPopupMenu(sharedFilesMenu, e) 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 = sharedFilesTable.getSelectionModel()
selectionModel.addListSelectionListener({ selectionModel.addListSelectionListener({
@@ -526,6 +533,14 @@ class MainFrameView {
return return
model.addCommentButtonEnabled = true 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
def searchesTable = builder.getVariable("searches-table") def searchesTable = builder.getVariable("searches-table")
@@ -555,27 +570,6 @@ class MainFrameView {
} }
}) })
// watched directories table
def watchedTable = builder.getVariable("watched-directories-table")
watchedTable.rowSorter.addRowSorterListener({evt -> lastWatchedSortEvent = evt})
watchedTable.rowSorter.setSortsOnUpdates(true)
JPopupMenu watchedMenu = new JPopupMenu()
JMenuItem stopWatching = new JMenuItem("Stop sharing")
stopWatching.addActionListener({mvcGroup.controller.stopWatchingDirectory()})
watchedMenu.add(stopWatching)
watchedTable.addMouseListener(new MouseAdapter() {
@Override
public void mouseReleased(MouseEvent e) {
if (e.isPopupTrigger())
showPopupMenu(watchedMenu, e)
}
@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")
subscriptionTable.setDefaultRenderer(Integer.class, centerRenderer) subscriptionTable.setDefaultRenderer(Integer.class, centerRenderer)
@@ -649,6 +643,9 @@ 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) {
@@ -656,22 +653,42 @@ class MainFrameView {
} }
def selectedSharedFiles() { def selectedSharedFiles() {
def sharedFilesTable = builder.getVariable("shared-files-table") if (!model.treeVisible) {
int[] selected = sharedFilesTable.getSelectedRows() def sharedFilesTable = builder.getVariable("shared-files-table")
if (selected.length == 0) int[] selected = sharedFilesTable.getSelectedRows()
return null if (selected.length == 0)
List<SharedFile> rv = new ArrayList<>() return null
if (lastSharedSortEvent != null) { List<SharedFile> rv = new ArrayList<>()
for (int i = 0; i < selected.length; i ++) { if (lastSharedSortEvent != null) {
selected[i] = sharedFilesTable.rowSorter.convertRowIndexToModel(selected[i]) 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
} }
selected.each {
rv.add(model.shared[it])
}
rv
} }
private static void getLeafs(TreeNode node, List<SharedFile> dest) {
if (node.isLeaf()) {
dest.add(node.getUserObject())
return
}
def children = node.children()
while(children.hasMoreElements()) {
getLeafs(children.nextElement(), dest)
}
}
def copyHashToClipboard() { def copyHashToClipboard() {
def selectedFiles = selectedSharedFiles() def selectedFiles = selectedSharedFiles()
if (selectedFiles == null) if (selectedFiles == null)
@@ -820,53 +837,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) chooser.setMultiSelectionEnabled(true)
int rv = chooser.showOpenDialog(null) int rv = chooser.showOpenDialog(null)
if (rv == JFileChooser.APPROVE_OPTION) { if (rv == JFileChooser.APPROVE_OPTION) {
chooser.getSelectedFiles().each { chooser.getSelectedFiles().each {
model.core.eventBus.publish(new FileSharedEvent(file : it)) 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)
chooser.setMultiSelectionEnabled(true)
int rv = chooser.showOpenDialog(null)
if (rv == JFileChooser.APPROVE_OPTION) {
chooser.getSelectedFiles().each { f ->
watchDirectory(f)
}
}
}
private void watchDirectory(File f) {
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()
@@ -876,4 +874,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

@@ -12,6 +12,7 @@ import javax.swing.SwingConstants
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 +36,9 @@ class OptionsView {
def updateField def updateField
def autoDownloadUpdateCheckbox def autoDownloadUpdateCheckbox
def shareDownloadedCheckbox def shareDownloadedCheckbox
def shareHiddenCheckbox
def searchCommentsCheckbox
def browseFilesCheckbox
def inboundLengthField def inboundLengthField
def inboundQuantityField def inboundQuantityField
@@ -46,6 +50,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
@@ -69,23 +74,32 @@ 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)) label(text : "Search in comments", constraints:gbc(gridx: 0, gridy:0))
retryField = textField(text : bind { model.downloadRetryInterval }, columns : 2, constraints : gbc(gridx: 1, gridy: 0)) searchCommentsCheckbox = checkBox(selected : bind {model.searchComments}, constraints : gbc(gridx:1, gridy:0))
label(text : "seconds", constraints : gbc(gridx : 2, gridy: 0))
label(text : "Retry failed downloads every", constraints : gbc(gridx: 0, gridy: 1))
retryField = textField(text : bind { model.downloadRetryInterval }, columns : 2, constraints : gbc(gridx: 1, gridy: 1))
label(text : "seconds", constraints : gbc(gridx : 2, gridy: 1))
label(text : "Check for updates every", constraints : gbc(gridx : 0, gridy: 1)) label(text : "Check for updates every", constraints : gbc(gridx : 0, gridy: 2))
updateField = textField(text : bind {model.updateCheckInterval }, columns : 2, constraints : gbc(gridx : 1, gridy: 1)) updateField = textField(text : bind {model.updateCheckInterval }, columns : 2, constraints : gbc(gridx : 1, gridy: 2))
label(text : "hours", constraints : gbc(gridx: 2, gridy : 1)) label(text : "hours", constraints : gbc(gridx: 2, gridy : 2))
label(text : "Download updates automatically", constraints: gbc(gridx :0, gridy : 2)) label(text : "Download updates automatically", constraints: gbc(gridx :0, gridy : 3))
autoDownloadUpdateCheckbox = checkBox(selected : bind {model.autoDownloadUpdate}, constraints : gbc(gridx:1, gridy : 2)) autoDownloadUpdateCheckbox = checkBox(selected : bind {model.autoDownloadUpdate}, constraints : gbc(gridx:1, gridy : 3))
label(text : "Share downloaded files", constraints : gbc(gridx : 0, gridy:3)) label(text : "Share downloaded files", constraints : gbc(gridx : 0, gridy:4))
shareDownloadedCheckbox = checkBox(selected : bind {model.shareDownloadedFiles}, constraints : gbc(gridx :1, gridy:3)) shareDownloadedCheckbox = checkBox(selected : bind {model.shareDownloadedFiles}, constraints : gbc(gridx :1, gridy:4))
label(text : "Share hidden files", constraints : gbc(gridx : 0, gridy:5))
shareHiddenCheckbox = checkBox(selected : bind {model.shareHiddenFiles}, constraints : gbc(gridx :1, gridy:5))
label(text : "Allow browsing", constraints : gbc(gridx : 0, gridy : 6))
browseFilesCheckbox = checkBox(selected : bind {model.browseFiles}, constraints : gbc(gridx : 1, gridy : 6))
label(text : "Save downloaded files to:", constraints: gbc(gridx:0, gridy:4)) label(text : "Save downloaded files to:", constraints: gbc(gridx:0, gridy:7))
button(text : "Choose", constraints : gbc(gridx : 1, gridy:4), downloadLocationAction) button(text : "Choose", constraints : gbc(gridx : 1, gridy:7), downloadLocationAction)
label(text : bind {model.downloadLocation}, constraints: gbc(gridx:0, gridy:5, gridwidth:2)) label(text : bind {model.downloadLocation}, constraints: gbc(gridx:0, gridy:8, gridwidth:2))
} }
i = builder.panel { i = builder.panel {
@@ -113,19 +127,28 @@ class OptionsView {
gridBagLayout() gridBagLayout()
label(text : "Changing these settings requires a restart", constraints : gbc(gridx : 0, gridy : 0, gridwidth: 2)) label(text : "Changing these settings requires a restart", constraints : gbc(gridx : 0, gridy : 0, gridwidth: 2))
label(text : "Look And Feel", constraints : gbc(gridx: 0, gridy:1)) label(text : "Look And Feel", constraints : gbc(gridx: 0, gridy:1))
lnfField = textField(text : bind {model.lnf}, columns : 4, constraints : gbc(gridx : 1, gridy : 1)) lnfField = textField(text : bind {model.lnf}, columns : 4, constraints : gbc(gridx : 1, gridy : 1, anchor : GridBagConstraints.LINE_START))
label(text : "Font", constraints : gbc(gridx: 0, gridy : 2)) label(text : "Font", constraints : gbc(gridx: 0, gridy : 2))
fontField = textField(text : bind {model.font}, columns : 4, constraints : gbc(gridx : 1, gridy:2)) fontField = textField(text : bind {model.font}, columns : 4, constraints : gbc(gridx : 1, gridy:2, anchor : GridBagConstraints.LINE_START))
// label(text : "Show Monitor", constraints : gbc(gridx :0, gridy: 3))
// monitorCheckbox = checkBox(selected : bind {model.showMonitor}, constraints : gbc(gridx : 1, gridy: 3)) label(text : "Font Size", constraints : gbc(gridx: 0, gridy : 3))
label(text : "Automatically Clear Cancelled Downloads", constraints: gbc(gridx: 0, gridy:4)) buttonGroup(id: "fontSizeGroup")
clearCancelledDownloadsCheckbox = checkBox(selected : bind {model.clearCancelledDownloads}, constraints : gbc(gridx : 1, gridy:4)) radioButton(text: "Automatic", selected : bind {model.automaticFontSize}, buttonGroup : fontSizeGroup,
label(text : "Automatically Clear Finished Downloads", constraints: gbc(gridx: 0, gridy:5)) constraints : gbc(gridx : 1, gridy: 3, anchor : GridBagConstraints.LINE_START), automaticFontAction)
clearFinishedDownloadsCheckbox = checkBox(selected : bind {model.clearFinishedDownloads}, constraints : gbc(gridx : 1, gridy:5)) radioButton(text: "Custom", selected : bind {!model.automaticFontSize}, buttonGroup : fontSizeGroup,
label(text : "Exclude Local Files From Results", constraints: gbc(gridx:0, gridy:6)) constraints : gbc(gridx : 1, gridy: 4, anchor : GridBagConstraints.LINE_START), customFontAction)
excludeLocalResultCheckbox = checkBox(selected : bind {model.excludeLocalResult}, constraints : gbc(gridx: 1, gridy : 6)) fontSizeField = textField(text : bind {model.customFontSize}, enabled : bind {!model.automaticFontSize}, constraints : gbc(gridx : 2, gridy : 4))
// label(text : "Show Hash Searches In Monitor", constraints: gbc(gridx:0, gridy:7))
// showSearchHashesCheckbox = checkBox(selected : bind {model.showSearchHashes}, constraints : gbc(gridx: 1, gridy: 7)) label(text : "Automatically Clear Cancelled Downloads", constraints: gbc(gridx: 0, gridy:5))
clearCancelledDownloadsCheckbox = checkBox(selected : bind {model.clearCancelledDownloads},
constraints : gbc(gridx : 1, gridy:5, anchor : GridBagConstraints.LINE_START))
label(text : "Automatically Clear Finished Downloads", constraints: gbc(gridx: 0, gridy:6))
clearFinishedDownloadsCheckbox = checkBox(selected : bind {model.clearFinishedDownloads},
constraints : gbc(gridx : 1, gridy:6, anchor : GridBagConstraints.LINE_START))
label(text : "Exclude Local Files From Results", constraints: gbc(gridx:0, gridy:7))
excludeLocalResultCheckbox = checkBox(selected : bind {model.excludeLocalResult},
constraints : gbc(gridx: 1, gridy : 7, anchor : GridBagConstraints.LINE_START))
} }
bandwidth = builder.panel { bandwidth = builder.panel {
gridBagLayout() gridBagLayout()

View File

@@ -63,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()
}) })
@@ -70,9 +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 : "Neutral", enabled: bind {model.trustButtonsEnabled}, neutralAction) panel {
button(text : "Distrust", enabled : bind {model.trustButtonsEnabled}, distrustAction) button(text : "Browse Host", enabled : bind {model.browseActionEnabled}, browseAction)
}
panel {
button(text : "Trust", enabled: bind {model.trustButtonsEnabled }, trustAction)
button(text : "Neutral", enabled: bind {model.trustButtonsEnabled}, neutralAction)
button(text : "Distrust", enabled : bind {model.trustButtonsEnabled}, distrustAction)
}
} }
} }
panel { panel {
@@ -193,12 +200,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()
} }
}) })
@@ -226,6 +235,9 @@ class SearchTabView {
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()})
menu.add(copyHashToClipboard) menu.add(copyHashToClipboard)
JMenuItem copyNameToClipboard = new JMenuItem("Copy name to clipboard")
copyNameToClipboard.addActionListener({mvcGroup.view.copyNameToClipboard()})
menu.add(copyNameToClipboard)
showMenu = true showMenu = true
// show comment if any // show comment if any
@@ -241,20 +253,36 @@ class SearchTabView {
if (showMenu) if (showMenu)
menu.show(e.getComponent(), e.getX(), e.getY()) menu.show(e.getComponent(), e.getX(), e.getY())
} }
def copyHashToClipboard() { private UIResultEvent getSelectedResult() {
int[] selectedRows = resultsTable.getSelectedRows() int[] selectedRows = resultsTable.getSelectedRows()
if (selectedRows.length != 1) if (selectedRows.length != 1)
return return null
int selected = selectedRows[0] int selected = selectedRows[0]
if (lastSortEvent != null) if (lastSortEvent != null)
selected = resultsTable.rowSorter.convertRowIndexToModel(selected) selected = resultsTable.rowSorter.convertRowIndexToModel(selected)
String hash = Base64.encode(model.results[selected].infohash.getRoot()) model.results[selected]
}
def copyHashToClipboard() {
def result = getSelectedResult()
if (result == null)
return
String hash = Base64.encode(result.infohash.getRoot())
StringSelection selection = new StringSelection(hash) 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)
}
def showComment() { def showComment() {
int selectedRow = resultsTable.getSelectedRow() int selectedRow = resultsTable.getSelectedRow()
if (selectedRow < 0) if (selectedRow < 0)

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)