Compare commits

...

37 Commits

Author SHA1 Message Date
Zlatin Balevsky
9c15208f3a Release 0.5.1 2019-10-19 19:11:04 +01:00
Zlatin Balevsky
a9ce9d96b3 wip on menu; close zlib stream 2019-10-19 18:54:58 +01:00
Zlatin Balevsky
4d2a5a8018 MainFrameModel doesn't need to listen to single result events anymore 2019-10-19 18:12:30 +01:00
Zlatin Balevsky
8395047386 compress results in browse connections 2019-10-19 17:59:08 +01:00
Zlatin Balevsky
cb23aa44f0 enable SEVERE log messages if no config file specified 2019-10-19 05:53:33 +01:00
Zlatin Balevsky
dbcb8508b8 add a view comment button 2019-10-19 05:35:04 +01:00
Zlatin Balevsky
47d406d93b add a border around the two panels 2019-10-19 04:59:37 +01:00
Zlatin Balevsky
e06f1805c2 redirect griffon logging to jul 2019-10-19 04:45:45 +01:00
Zlatin Balevsky
2b04374e23 add option to disable browsing of files, make the dialog bigger 2019-10-19 00:53:13 +01:00
Zlatin Balevsky
383addbc37 implement view comment from browse window 2019-10-19 00:30:03 +01:00
Zlatin Balevsky
cc39cd7f8e implement downloading from browse window 2019-10-19 00:23:43 +01:00
Zlatin Balevsky
83665d7524 wip on browse host 2019-10-18 23:55:07 +01:00
Zlatin Balevsky
94340480b4 wip on browse host 2019-10-18 23:25:26 +01:00
Zlatin Balevsky
8850d49c63 wip on browse host 2019-10-18 23:16:37 +01:00
Zlatin Balevsky
f0f9d840f0 wip on browse host 2019-10-18 22:35:17 +01:00
Zlatin Balevsky
7f4cd4f331 wip on browse host 2019-10-18 21:17:34 +01:00
Zlatin Balevsky
e6162503f6 wip on browse host 2019-10-18 20:29:39 +01:00
Zlatin Balevsky
7a5d71dc36 add copy name to clipboard option 2019-10-17 19:01:53 +01:00
Zlatin Balevsky
6fa39a5e35 turn off logging if there is no config file 2019-10-17 18:39:28 +01:00
Zlatin Balevsky
c5ae804f61 Implement automatic font sizing; set all font properties on change of font 2019-10-17 18:15:04 +01:00
Zlatin Balevsky
d7695b448d remove my DS_Store 2019-10-17 05:50:29 +01:00
Zlatin Balevsky
946d9c8f32 disable sharing of hidden files by default, add option to enable 2019-10-17 05:46:27 +01:00
Zlatin Balevsky
02441ca1e3 add option to disable searching in comments 2019-10-16 19:57:18 +01:00
Zlatin Balevsky
5fa21b2360 keep tree expanded on modifications 2019-10-16 14:42:40 +01:00
Zlatin Balevsky
d4c08f4fe6 only remove from index if no more files have the same comment pt.2 2019-10-16 14:23:12 +01:00
Zlatin Balevsky
942de287c6 only remove from index if no more files have the same comment 2019-10-16 14:21:50 +01:00
Zlatin Balevsky
d0299f80c6 search through comments 2019-10-16 14:06:11 +01:00
Zlatin Balevsky
1227cf9263 Release 0.5.0 2019-10-15 12:38:25 +01:00
Zlatin Balevsky
a05575485f move things around 2019-10-15 10:40:50 +01:00
Zlatin Balevsky
f5bccd8126 All shared directories are watched directories. Fix manipulation of tree structure 2019-10-15 08:38:23 +01:00
Zlatin Balevsky
70fb789abf remove the watched directories table 2019-10-15 04:51:21 +01:00
Zlatin Balevsky
feb712c253 Move persisting of files on dedicated thread. Introduce an event to forcefully persist files. Do that immediately after unsharing anything 2019-10-15 04:21:40 +01:00
Zlatin Balevsky
d22b403e2a stop watching multiple directories at once 2019-10-14 23:16:05 +01:00
Zlatin Balevsky
a24982e0df fix comments for local results 2019-10-14 22:47:52 +01:00
Zlatin Balevsky
6c26019164 allow switching without restart 2019-10-14 21:40:03 +01:00
Zlatin Balevsky
965fa79bbf fix count of shared files in tree view mode 2019-10-14 20:57:50 +01:00
Zlatin Balevsky
60ddb85461 Tree view of the shared files. The count is wrong for some reason 2019-10-14 20:13:25 +01:00
42 changed files with 1229 additions and 309 deletions

View File

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

View File

@@ -53,7 +53,7 @@ class CliDownloader {
Core core
try {
core = new Core(props, home, "0.4.16")
core = new Core(props, home, "0.5.1")
} catch (Exception bad) {
bad.printStackTrace(System.out)
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.HasherService
import com.muwire.core.files.PersisterService
import com.muwire.core.files.UICommentEvent
import com.muwire.core.files.UIPersistFilesEvent
import com.muwire.core.files.AllFilesLoadedEvent
import com.muwire.core.files.DirectoryUnsharedEvent
import com.muwire.core.files.DirectoryWatcher
@@ -35,11 +37,13 @@ import com.muwire.core.hostcache.CacheClient
import com.muwire.core.hostcache.HostCache
import com.muwire.core.hostcache.HostDiscoveredEvent
import com.muwire.core.mesh.MeshManager
import com.muwire.core.search.BrowseManager
import com.muwire.core.search.QueryEvent
import com.muwire.core.search.ResultsEvent
import com.muwire.core.search.ResultsSender
import com.muwire.core.search.SearchEvent
import com.muwire.core.search.SearchManager
import com.muwire.core.search.UIBrowseEvent
import com.muwire.core.search.UIResultBatchEvent
import com.muwire.core.trust.TrustEvent
import com.muwire.core.trust.TrustService
@@ -215,6 +219,7 @@ public class Core {
eventBus.register(FileUnsharedEvent.class, fileManager)
eventBus.register(SearchEvent.class, fileManager)
eventBus.register(DirectoryUnsharedEvent.class, fileManager)
eventBus.register(UICommentEvent.class, fileManager)
log.info("initializing mesh manager")
MeshManager meshManager = new MeshManager(fileManager, home, props)
@@ -223,6 +228,7 @@ public class Core {
log.info "initializing persistence service"
persisterService = new PersisterService(new File(home, "files.json"), eventBus, 60000, fileManager)
eventBus.register(UILoadedEvent.class, persisterService)
eventBus.register(UIPersistFilesEvent.class, persisterService)
log.info("initializing host cache")
File hostStorage = new File(home, "hosts.json")
@@ -251,7 +257,7 @@ public class Core {
I2PConnector i2pConnector = new I2PConnector(socketManager)
log.info "initializing results sender"
ResultsSender resultsSender = new ResultsSender(eventBus, i2pConnector, me)
ResultsSender resultsSender = new ResultsSender(eventBus, i2pConnector, me, props)
log.info "initializing search manager"
SearchManager searchManager = new SearchManager(eventBus, me, resultsSender)
@@ -277,17 +283,19 @@ public class Core {
log.info("initializing acceptor")
I2PAcceptor i2pAcceptor = new I2PAcceptor(socketManager)
connectionAcceptor = new ConnectionAcceptor(eventBus, connectionManager, props,
i2pAcceptor, hostCache, trustService, searchManager, uploadManager, connectionEstablisher)
i2pAcceptor, hostCache, trustService, searchManager, uploadManager, fileManager, connectionEstablisher)
log.info("initializing directory watcher")
directoryWatcher = new DirectoryWatcher(eventBus, fileManager)
directoryWatcher = new DirectoryWatcher(eventBus, fileManager, home, props)
eventBus.register(FileSharedEvent.class, directoryWatcher)
eventBus.register(AllFilesLoadedEvent.class, directoryWatcher)
eventBus.register(DirectoryUnsharedEvent.class, directoryWatcher)
log.info("initializing hasher service")
hasherService = new HasherService(new FileHasher(), eventBus, fileManager)
hasherService = new HasherService(new FileHasher(), eventBus, fileManager, props)
eventBus.register(FileSharedEvent.class, hasherService)
eventBus.register(FileUnsharedEvent.class, hasherService)
eventBus.register(DirectoryUnsharedEvent.class, hasherService)
log.info("initializing trust subscriber")
trustSubscriber = new TrustSubscriber(eventBus, i2pConnector, props)
@@ -298,6 +306,11 @@ public class Core {
contentManager = new ContentManager()
eventBus.register(ContentControlEvent.class, contentManager)
eventBus.register(QueryEvent.class, contentManager)
log.info("initializing browse manager")
BrowseManager browseManager = new BrowseManager(i2pConnector, eventBus)
eventBus.register(UIBrowseEvent.class, browseManager)
}
public void startServices() {
@@ -362,7 +375,7 @@ public class Core {
}
}
Core core = new Core(props, home, "0.4.16")
Core core = new Core(props, home, "0.5.1")
core.startServices()
// ... 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 net.i2p.data.Base64
import net.i2p.util.ConcurrentHashSet
class MuWireSettings {
@@ -23,6 +24,9 @@ class MuWireSettings {
File downloadLocation
CrawlerResponse crawlerResponse
boolean shareDownloadedFiles
boolean shareHiddenFiles
boolean searchComments
boolean browseFiles
Set<String> watchedDirectories
float downloadSequentialRatio
int hostClearInterval, hostHopelessInterval, hostRejectInterval
@@ -51,6 +55,7 @@ class MuWireSettings {
autoDownloadUpdate = Boolean.parseBoolean(props.getProperty("autoDownloadUpdate","true"))
updateType = props.getProperty("updateType","jar")
shareDownloadedFiles = Boolean.parseBoolean(props.getProperty("shareDownloadedFiles","true"))
shareHiddenFiles = Boolean.parseBoolean(props.getProperty("shareHiddenFiles","false"))
downloadSequentialRatio = Float.valueOf(props.getProperty("downloadSequentialRatio","0.8"))
hostClearInterval = Integer.valueOf(props.getProperty("hostClearInterval","15"))
hostHopelessInterval = Integer.valueOf(props.getProperty("hostHopelessInterval", "1440"))
@@ -59,6 +64,8 @@ class MuWireSettings {
embeddedRouter = Boolean.valueOf(props.getProperty("embeddedRouter","false"))
inBw = Integer.valueOf(props.getProperty("inBw","256"))
outBw = Integer.valueOf(props.getProperty("outBw","128"))
searchComments = Boolean.valueOf(props.getProperty("searchComments","true"))
browseFiles = Boolean.valueOf(props.getProperty("browseFiles","true"))
watchedDirectories = readEncodedSet(props, "watchedDirectories")
watchedKeywords = readEncodedSet(props, "watchedKeywords")
@@ -89,6 +96,7 @@ class MuWireSettings {
props.setProperty("autoDownloadUpdate", String.valueOf(autoDownloadUpdate))
props.setProperty("updateType",String.valueOf(updateType))
props.setProperty("shareDownloadedFiles", String.valueOf(shareDownloadedFiles))
props.setProperty("shareHiddenFiles", String.valueOf(shareHiddenFiles))
props.setProperty("downloadSequentialRatio", String.valueOf(downloadSequentialRatio))
props.setProperty("hostClearInterval", String.valueOf(hostClearInterval))
props.setProperty("hostHopelessInterval", String.valueOf(hostHopelessInterval))
@@ -97,6 +105,8 @@ class MuWireSettings {
props.setProperty("embeddedRouter", String.valueOf(embeddedRouter))
props.setProperty("inBw", String.valueOf(inBw))
props.setProperty("outBw", String.valueOf(outBw))
props.setProperty("searchComments", String.valueOf(searchComments))
props.setProperty("browseFiles", String.valueOf(browseFiles))
writeEncodedSet(watchedDirectories, "watchedDirectories", props)
writeEncodedSet(watchedKeywords, "watchedKeywords", props)
@@ -113,7 +123,7 @@ class MuWireSettings {
}
private static Set<String> readEncodedSet(Properties props, String property) {
Set<String> rv = new HashSet<>()
Set<String> rv = new ConcurrentHashSet<>()
if (props.containsKey(property)) {
String[] encoded = props.getProperty(property).split(",")
encoded.each { rv << DataUtil.readi18nString(Base64.decode(it)) }

View File

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

View File

@@ -5,11 +5,13 @@ import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import java.util.logging.Level
import java.util.zip.DeflaterOutputStream
import java.util.zip.GZIPOutputStream
import java.util.zip.InflaterInputStream
import com.muwire.core.EventBus
import com.muwire.core.MuWireSettings
import com.muwire.core.Persona
import com.muwire.core.files.FileManager
import com.muwire.core.hostcache.HostCache
import com.muwire.core.trust.TrustLevel
import com.muwire.core.trust.TrustService
@@ -17,6 +19,7 @@ import com.muwire.core.upload.UploadManager
import com.muwire.core.util.DataUtil
import com.muwire.core.search.InvalidSearchResultException
import com.muwire.core.search.ResultsParser
import com.muwire.core.search.ResultsSender
import com.muwire.core.search.SearchManager
import com.muwire.core.search.UIResultBatchEvent
import com.muwire.core.search.UIResultEvent
@@ -37,6 +40,7 @@ class ConnectionAcceptor {
final TrustService trustService
final SearchManager searchManager
final UploadManager uploadManager
final FileManager fileManager
final ConnectionEstablisher establisher
final ExecutorService acceptorThread
@@ -47,7 +51,7 @@ class ConnectionAcceptor {
ConnectionAcceptor(EventBus eventBus, UltrapeerConnectionManager manager,
MuWireSettings settings, I2PAcceptor acceptor, HostCache hostCache,
TrustService trustService, SearchManager searchManager, UploadManager uploadManager,
ConnectionEstablisher establisher) {
FileManager fileManager, ConnectionEstablisher establisher) {
this.eventBus = eventBus
this.manager = manager
this.settings = settings
@@ -55,6 +59,7 @@ class ConnectionAcceptor {
this.hostCache = hostCache
this.trustService = trustService
this.searchManager = searchManager
this.fileManager = fileManager
this.uploadManager = uploadManager
this.establisher = establisher
@@ -129,6 +134,9 @@ class ConnectionAcceptor {
case (byte)'T':
processTRUST(e)
break
case (byte)'B':
processBROWSE(e)
break
default:
throw new Exception("Invalid read $read")
}
@@ -246,44 +254,87 @@ class ConnectionAcceptor {
e.close()
}
}
private void processBROWSE(Endpoint e) {
try {
byte [] rowse = new byte[7]
DataInputStream dis = new DataInputStream(e.getInputStream())
dis.readFully(rowse)
if (rowse != "ROWSE\r\n".getBytes(StandardCharsets.US_ASCII))
throw new IOException("Invalid BROWSE connection")
String header
while ((header = DataUtil.readTillRN(dis)) != ""); // ignore headers for now
OutputStream os = e.getOutputStream()
if (!settings.browseFiles) {
os.write("403 Not Allowed\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
os.flush()
e.close()
return
}
os.write("200 OK\r\n".getBytes(StandardCharsets.US_ASCII))
def sharedFiles = fileManager.getSharedFiles().values()
os.write("Count: ${sharedFiles.size()}\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
DataOutputStream dos = new DataOutputStream(new GZIPOutputStream(os))
JsonOutput jsonOutput = new JsonOutput()
sharedFiles.each {
def obj = ResultsSender.sharedFileToObj(it, false)
def json = jsonOutput.toJson(obj)
dos.writeShort((short)json.length())
dos.write(json.getBytes(StandardCharsets.US_ASCII))
}
dos.flush()
dos.close()
} finally {
e.close()
}
}
private void processTRUST(Endpoint e) {
byte[] RUST = new byte[6]
DataInputStream dis = new DataInputStream(e.getInputStream())
dis.readFully(RUST)
if (RUST != "RUST\r\n".getBytes(StandardCharsets.US_ASCII))
throw new IOException("Invalid TRUST connection")
String header
while ((header = DataUtil.readTillRN(dis)) != ""); // ignore headers for now
try {
byte[] RUST = new byte[6]
DataInputStream dis = new DataInputStream(e.getInputStream())
dis.readFully(RUST)
if (RUST != "RUST\r\n".getBytes(StandardCharsets.US_ASCII))
throw new IOException("Invalid TRUST connection")
String header
while ((header = DataUtil.readTillRN(dis)) != ""); // ignore headers for now
OutputStream os = e.getOutputStream()
if (!settings.allowTrustLists) {
os.write("403 Not Allowed\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
os.flush()
OutputStream os = e.getOutputStream()
if (!settings.allowTrustLists) {
os.write("403 Not Allowed\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
os.flush()
e.close()
return
}
os.write("200 OK\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
List<Persona> good = new ArrayList<>(trustService.good.values())
int size = Math.min(Short.MAX_VALUE * 2, good.size())
good = good.subList(0, size)
DataOutputStream dos = new DataOutputStream(os)
dos.writeShort(size)
good.each {
it.write(dos)
}
List<Persona> bad = new ArrayList<>(trustService.bad.values())
size = Math.min(Short.MAX_VALUE * 2, bad.size())
bad = bad.subList(0, size)
dos.writeShort(size)
bad.each {
it.write(dos)
}
dos.flush()
} finally {
e.close()
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 com.muwire.core.EventBus
import com.muwire.core.MuWireSettings
import com.muwire.core.SharedFile
import groovy.util.logging.Log
@@ -31,6 +32,8 @@ class DirectoryWatcher {
kinds = [ENTRY_CREATE, ENTRY_MODIFY, ENTRY_DELETE]
}
private final File home
private final MuWireSettings muOptions
private final EventBus eventBus
private final FileManager fileManager
private final Thread watcherThread, publisherThread
@@ -39,7 +42,9 @@ class DirectoryWatcher {
private WatchService watchService
private volatile boolean shutdown
DirectoryWatcher(EventBus eventBus, FileManager fileManager) {
DirectoryWatcher(EventBus eventBus, FileManager fileManager, File home, MuWireSettings muOptions) {
this.home = home
this.muOptions = muOptions
this.eventBus = eventBus
this.fileManager = fileManager
this.watcherThread = new Thread({watch() } as Runnable, "directory-watcher")
@@ -64,15 +69,28 @@ class DirectoryWatcher {
void onFileSharedEvent(FileSharedEvent e) {
if (!e.file.isDirectory())
return
Path path = e.file.getCanonicalFile().toPath()
File canonical = e.file.getCanonicalFile()
Path path = canonical.toPath()
WatchKey wk = path.register(watchService, kinds)
watchedDirectories.put(e.file, wk)
watchedDirectories.put(canonical, wk)
if (muOptions.watchedDirectories.add(canonical.toString()))
saveMuSettings()
}
void onDirectoryUnsharedEvent(DirectoryUnsharedEvent e) {
WatchKey wk = watchedDirectories.remove(e.directory)
wk?.cancel()
if (muOptions.watchedDirectories.remove(e.directory.toString()))
saveMuSettings()
}
private void saveMuSettings() {
File muSettingsFile = new File(home, "MuWire.properties")
muSettingsFile.withOutputStream {
muOptions.write(it)
}
}
private void watch() {

View File

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

View File

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

View File

@@ -3,6 +3,9 @@ package com.muwire.core.files
import java.nio.file.CopyOption
import java.nio.file.Files
import java.nio.file.StandardCopyOption
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import java.util.concurrent.ThreadFactory
import java.util.logging.Level
import java.util.stream.Collectors
@@ -28,13 +31,16 @@ class PersisterService extends Service {
final int interval
final Timer timer
final FileManager fileManager
final ExecutorService persisterExecutor = Executors.newSingleThreadExecutor({ r ->
new Thread(r, "file persister")
} as ThreadFactory)
PersisterService(File location, EventBus listener, int interval, FileManager fileManager) {
this.location = location
this.listener = listener
this.interval = interval
this.fileManager = fileManager
timer = new Timer("file persister", true)
timer = new Timer("file persister timer", true)
}
void stop() {
@@ -44,6 +50,10 @@ class PersisterService extends Service {
void onUILoadedEvent(UILoadedEvent e) {
timer.schedule({load()} as TimerTask, 1)
}
void onUIPersistFilesEvent(UIPersistFilesEvent e) {
persistFiles()
}
void load() {
Thread.currentThread().setPriority(Thread.MIN_PRIORITY)
@@ -127,19 +137,21 @@ class PersisterService extends Service {
}
private void persistFiles() {
def sharedFiles = fileManager.getSharedFiles()
persisterExecutor.submit( {
def sharedFiles = fileManager.getSharedFiles()
File tmp = File.createTempFile("muwire-files", "tmp")
tmp.deleteOnExit()
tmp.withPrintWriter { writer ->
sharedFiles.each { k, v ->
def json = toJson(k,v)
json = JsonOutput.toJson(json)
writer.println json
File tmp = File.createTempFile("muwire-files", "tmp")
tmp.deleteOnExit()
tmp.withPrintWriter { writer ->
sharedFiles.each { k, v ->
def json = toJson(k,v)
json = JsonOutput.toJson(json)
writer.println json
}
}
}
Files.copy(tmp.toPath(), location.toPath(), StandardCopyOption.REPLACE_EXISTING)
tmp.delete()
Files.copy(tmp.toPath(), location.toPath(), StandardCopyOption.REPLACE_EXISTING)
tmp.delete()
} as Runnable)
}
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,87 @@
package com.muwire.core.search
import com.muwire.core.Constants
import com.muwire.core.EventBus
import com.muwire.core.connection.Endpoint
import com.muwire.core.connection.I2PConnector
import com.muwire.core.util.DataUtil
import groovy.json.JsonSlurper
import groovy.util.logging.Log
import java.nio.charset.StandardCharsets
import java.util.concurrent.Executor
import java.util.concurrent.Executors
import java.util.logging.Level
import java.util.zip.GZIPInputStream
@Log
class BrowseManager {
private final I2PConnector connector
private final EventBus eventBus
private final Executor browserThread = Executors.newSingleThreadExecutor()
BrowseManager(I2PConnector connector, EventBus eventBus) {
this.connector = connector
this.eventBus = eventBus
}
void onUIBrowseEvent(UIBrowseEvent e) {
browserThread.execute({
Endpoint endpoint = null
try {
eventBus.publish(new BrowseStatusEvent(status : BrowseStatus.CONNECTING))
endpoint = connector.connect(e.host.destination)
OutputStream os = endpoint.getOutputStream()
os.write("BROWSE\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
InputStream is = endpoint.getInputStream()
String code = DataUtil.readTillRN(is)
if (!code.startsWith("200"))
throw new IOException("Invalid code")
// 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(new GZIPInputStream(is))
UUID uuid = UUID.randomUUID()
for (int i = 0; i < results; i++) {
int size = dis.readUnsignedShort()
byte [] tmp = new byte[size]
dis.readFully(tmp)
def json = slurper.parse(tmp)
UIResultEvent result = ResultsParser.parse(e.host, uuid, json)
eventBus.publish(result)
}
eventBus.publish(new BrowseStatusEvent(status : BrowseStatus.FINISHED))
} catch (Exception bad) {
log.log(Level.WARNING, "browse failed", bad)
eventBus.publish(new BrowseStatusEvent(status : BrowseStatus.FAILED))
} finally {
endpoint?.close()
}
} as Runnable)
}
}

View File

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

View File

@@ -0,0 +1,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
if (json.comment != null)
comment = DataUtil.readi18nString(Base64.decode(json.comment))
boolean browse = false
if (json.browse != null)
browse = true
return new UIResultEvent( sender : p,
name : name,
@@ -102,6 +106,7 @@ class ResultsParser {
pieceSize : pieceSize,
sources : sources,
comment : comment,
browse : browse,
uuid: uuid)
} catch (Exception 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.I2PConnector
import com.muwire.core.files.FileHasher
import com.muwire.core.util.DataUtil
import com.muwire.core.Persona
import java.nio.charset.StandardCharsets
@@ -17,6 +18,7 @@ import java.util.stream.Collectors
import com.muwire.core.DownloadedFile
import com.muwire.core.EventBus
import com.muwire.core.InfoHash
import com.muwire.core.MuWireSettings
import groovy.json.JsonOutput
import groovy.util.logging.Log
@@ -42,11 +44,13 @@ class ResultsSender {
private final I2PConnector connector
private final Persona me
private final EventBus eventBus
private final MuWireSettings settings
ResultsSender(EventBus eventBus, I2PConnector connector, Persona me) {
ResultsSender(EventBus eventBus, I2PConnector connector, Persona me, MuWireSettings settings) {
this.connector = connector;
this.eventBus = eventBus
this.me = me
this.settings = settings
}
void sendResults(UUID uuid, SharedFile[] results, Destination target, boolean oobInfohash) {
@@ -60,13 +64,18 @@ class ResultsSender {
Set<Destination> suggested = Collections.emptySet()
if (it instanceof DownloadedFile)
suggested = it.sources
def comment = null
if (it.getComment() != null) {
comment = DataUtil.readi18nString(Base64.decode(it.getComment()))
}
def uiResultEvent = new UIResultEvent( sender : me,
name : it.getFile().getName(),
size : length,
infohash : it.getInfoHash(),
pieceSize : pieceSize,
uuid : uuid,
sources : suggested
sources : suggested,
comment : comment
)
eventBus.publish(uiResultEvent)
}
@@ -85,7 +94,6 @@ class ResultsSender {
@Override
public void run() {
try {
byte [] tmp = new byte[InfoHash.SIZE]
JsonOutput jsonOutput = new JsonOutput()
Endpoint endpoint = null;
try {
@@ -95,36 +103,7 @@ class ResultsSender {
me.write(os)
os.writeShort((short)results.length)
results.each {
byte [] name = it.getFile().getName().getBytes(StandardCharsets.UTF_8)
def baos = new ByteArrayOutputStream()
def daos = new DataOutputStream(baos)
daos.writeShort((short) name.length)
daos.write(name)
daos.flush()
String encodedName = Base64.encode(baos.toByteArray())
def obj = [:]
obj.type = "Result"
obj.version = oobInfohash ? 2 : 1
obj.name = encodedName
obj.infohash = Base64.encode(it.getInfoHash().getRoot())
obj.size = it.getFile().length()
obj.pieceSize = it.getPieceSize()
if (!oobInfohash) {
byte [] hashList = it.getInfoHash().getHashList()
def hashListB64 = []
for (int i = 0; i < hashList.length / InfoHash.SIZE; i++) {
System.arraycopy(hashList, InfoHash.SIZE * i, tmp, 0, InfoHash.SIZE)
hashListB64 << Base64.encode(tmp)
}
obj.hashList = hashListB64
}
if (it instanceof DownloadedFile)
obj.sources = it.sources.stream().map({dest -> dest.toBase64()}).collect(Collectors.toSet())
if (it.getComment() != null)
obj.comment = it.getComment()
def obj = sharedFileToObj(it, settings.browseFiles)
def json = jsonOutput.toJson(obj)
os.writeShort((short)json.length())
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
UUID uuid
boolean oobInfohash
boolean searchComments
String toString() {
def infoHash = null
if (searchHash != null)
infoHash = new InfoHash(searchHash)
"searchTerms: $searchTerms searchHash:$infoHash, uuid:$uuid oobInfohash:$oobInfohash"
"searchTerms: $searchTerms searchHash:$infoHash, uuid:$uuid oobInfohash:$oobInfohash searchComments:$searchComments"
}
}

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
int pieceSize
String comment
boolean browse
@Override
public String toString() {

View File

@@ -25,7 +25,8 @@ class HasherServiceTest {
void before() {
eventBus = new EventBus()
hasher = new FileHasher()
service = new HasherService(hasher, eventBus, new FileManager(eventBus, new MuWireSettings()))
def props = new MuWireSettings()
service = new HasherService(hasher, eventBus, new FileManager(eventBus, props), props)
eventBus.register(FileHashedEvent.class, listener)
eventBus.register(FileSharedEvent.class, service)
service.start()

View File

@@ -1,5 +1,5 @@
group = com.muwire
version = 0.4.16
version = 0.5.1
groovyVersion = 2.4.15
slf4jVersion = 1.7.25
spockVersion = 1.1-groovy-2.4

View File

@@ -58,7 +58,11 @@ dependencies {
compile project(":core")
compile "org.codehaus.griffon:griffon-guice:${griffon.version}"
runtime "org.slf4j:slf4j-simple:${slf4jVersion}"
// runtime "org.slf4j:slf4j-simple:${slf4jVersion}"
runtime group: 'org.slf4j', name: 'slf4j-jdk14', version: "${slf4jVersion}"
runtime group: 'org.slf4j', name: 'slf4j-api', version: "${slf4jVersion}"
runtime group: 'org.slf4j', name: 'jul-to-slf4j', version: "${slf4jVersion}"
runtime "javax.annotation:javax.annotation-api:1.3.2"
testCompile "org.codehaus.griffon:griffon-fest-test:${griffon.version}"

View File

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

View File

@@ -8,6 +8,8 @@ import net.i2p.data.Base64
import javax.annotation.Nonnull
import com.muwire.core.Core
import com.muwire.core.files.UICommentEvent
import com.muwire.core.util.DataUtil
@ArtifactProviderFor(GriffonController)
@@ -17,6 +19,8 @@ class AddCommentController {
@MVCMember @Nonnull
AddCommentView view
Core core
@ControllerAction
void save() {
String comment = view.textarea.getText()
@@ -25,9 +29,11 @@ class AddCommentController {
else
comment = Base64.encode(DataUtil.encodei18nString(comment))
model.selectedFiles.each {
def event = new UICommentEvent(sharedFile : it, oldComment : it.getComment())
it.setComment(comment)
core.eventBus.publish(event)
}
mvcGroup.parentGroup.view.builder.getVariable("shared-files-table").model.fireTableDataChanged()
mvcGroup.parentGroup.view.refreshSharedFiles()
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.files.DirectoryUnsharedEvent
import com.muwire.core.files.FileUnsharedEvent
import com.muwire.core.files.UIPersistFilesEvent
import com.muwire.core.search.QueryEvent
import com.muwire.core.search.SearchEvent
import com.muwire.core.trust.RemoteTrustList
@@ -84,7 +85,8 @@ class MainFrameController {
def terms = replaced.split(" ")
def nonEmpty = []
terms.each { if (it.length() > 0) nonEmpty << it }
searchEvent = new SearchEvent(searchTerms : nonEmpty, uuid : uuid, oobInfohash: true)
searchEvent = new SearchEvent(searchTerms : nonEmpty, uuid : uuid, oobInfohash: true,
searchComments : core.muOptions.searchComments)
}
boolean firstHop = core.muOptions.allowUntrusted || core.muOptions.searchExtraHop
core.eventBus.publish(new QueryEvent(searchEvent : searchEvent, firstHop : firstHop,
@@ -276,6 +278,7 @@ class MainFrameController {
sf.each {
core.eventBus.publish(new FileUnsharedEvent(unsharedFile : it))
}
core.eventBus.publish(new UIPersistFilesEvent())
}
@ControllerAction
@@ -286,21 +289,10 @@ class MainFrameController {
Map<String, Object> params = new HashMap<>()
params['selectedFiles'] = selectedFiles
params['core'] = core
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() {
File f = new File(core.home, "MuWire.properties")
f.withOutputStream {

View File

@@ -70,6 +70,10 @@ class OptionsController {
model.updateCheckInterval = text
settings.updateCheckInterval = Integer.valueOf(text)
boolean searchComments = view.searchCommentsCheckbox.model.isSelected()
model.searchComments = searchComments
settings.searchComments = searchComments
boolean autoDownloadUpdate = view.autoDownloadUpdateCheckbox.model.isSelected()
model.autoDownloadUpdate = autoDownloadUpdate
settings.autoDownloadUpdate = autoDownloadUpdate
@@ -78,7 +82,15 @@ class OptionsController {
boolean shareDownloaded = view.shareDownloadedCheckbox.model.isSelected()
model.shareDownloadedFiles = shareDownloaded
settings.shareDownloadedFiles = shareDownloaded
boolean shareHidden = view.shareHiddenCheckbox.model.isSelected()
model.shareHiddenFiles = shareHidden
settings.shareHiddenFiles = shareHidden
boolean browseFiles = view.browseFilesCheckbox.model.isSelected()
model.browseFiles = browseFiles
settings.browseFiles = browseFiles
String downloadLocation = model.downloadLocation
settings.downloadLocation = new File(downloadLocation)
@@ -123,10 +135,9 @@ class OptionsController {
text = view.fontField.text
model.font = text
uiSettings.font = text
// boolean showMonitor = view.monitorCheckbox.model.isSelected()
// model.showMonitor = showMonitor
// uiSettings.showMonitor = showMonitor
uiSettings.autoFontSize = model.automaticFontSize
uiSettings.fontSize = Integer.parseInt(view.fontSizeField.text)
boolean clearCancelledDownloads = view.clearCancelledDownloadsCheckbox.model.isSelected()
model.clearCancelledDownloads = clearCancelledDownloads
@@ -140,10 +151,6 @@ class OptionsController {
model.excludeLocalResult = excludeLocalResult
uiSettings.excludeLocalResult = excludeLocalResult
// boolean showSearchHashes = view.showSearchHashesCheckbox.model.isSelected()
// model.showSearchHashes = showSearchHashes
// uiSettings.showSearchHashes = showSearchHashes
File uiSettingsFile = new File(core.home, "gui.properties")
uiSettingsFile.withOutputStream {
uiSettings.write(it)
@@ -167,5 +174,16 @@ class OptionsController {
int rv = chooser.showOpenDialog(null)
if (rv == JFileChooser.APPROVE_OPTION)
model.downloadLocation = chooser.getSelectedFile().getAbsolutePath()
}
@ControllerAction
void automaticFontAction() {
model.automaticFontSize = true
model.customFontSize = 12
}
@ControllerAction
void customFontAction() {
model.automaticFontSize = false
}
}

View File

@@ -4,9 +4,12 @@ import griffon.core.artifact.GriffonController
import griffon.core.controller.ControllerAction
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import net.i2p.data.Base64
import javax.annotation.Nonnull
import com.muwire.core.Core
import com.muwire.core.Persona
import com.muwire.core.download.UIDownloadEvent
import com.muwire.core.search.UIResultEvent
import com.muwire.core.trust.TrustEvent
@@ -85,4 +88,37 @@ class SearchTabController {
def sender = model.senders[row]
core.eventBus.publish( new TrustEvent(persona : sender, level : TrustLevel.NEUTRAL))
}
@ControllerAction
void browse() {
int selectedSender = view.selectedSenderRow()
if (selectedSender < 0)
return
Persona sender = model.senders[selectedSender]
String groupId = sender.getHumanReadableName()
Map<String,Object> params = new HashMap<>()
params['host'] = sender
params['eventBus'] = core.eventBus
mvcGroup.createMVCGroup("browse", groupId, params)
}
@ControllerAction
void showComment() {
int[] selectedRows = view.resultsTable.getSelectedRows()
if (selectedRows.length != 1)
return
if (view.lastSortEvent != null)
selectedRows[0] = view.resultsTable.rowSorter.convertRowIndexToModel(selectedRows[0])
UIResultEvent event = model.results[selectedRows[0]]
if (event.comment == null)
return
String groupId = Base64.encode(event.infohash.getRoot())
Map<String,Object> params = new HashMap<>()
params['result'] = event
mvcGroup.createMVCGroup("show-comment", groupId, params)
}
}

View File

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

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
import java.util.concurrent.ConcurrentHashMap
import java.nio.file.Path
import java.nio.file.Paths
import java.util.Calendar
import java.util.UUID
@@ -8,12 +10,16 @@ import javax.annotation.Nonnull
import javax.inject.Inject
import javax.swing.JOptionPane
import javax.swing.JTable
import javax.swing.tree.DefaultMutableTreeNode
import javax.swing.tree.DefaultTreeModel
import javax.swing.tree.TreeNode
import com.muwire.core.Core
import com.muwire.core.InfoHash
import com.muwire.core.MuWireSettings
import com.muwire.core.Persona
import com.muwire.core.RouterDisconnectedEvent
import com.muwire.core.SharedFile
import com.muwire.core.connection.ConnectionAttemptStatus
import com.muwire.core.connection.ConnectionEvent
import com.muwire.core.connection.DisconnectionEvent
@@ -21,6 +27,7 @@ import com.muwire.core.content.ContentControlEvent
import com.muwire.core.download.DownloadStartedEvent
import com.muwire.core.download.Downloader
import com.muwire.core.files.AllFilesLoadedEvent
import com.muwire.core.files.DirectoryUnsharedEvent
import com.muwire.core.files.FileDownloadedEvent
import com.muwire.core.files.FileHashedEvent
import com.muwire.core.files.FileHashingEvent
@@ -57,6 +64,8 @@ class MainFrameModel {
FactoryBuilderSupport builder
@MVCMember @Nonnull
MainFrameController controller
@MVCMember @Nonnull
MainFrameView view
@Inject @Nonnull GriffonApplication application
@Observable boolean coreInitialized = false
@Observable boolean routerPresent
@@ -64,8 +73,11 @@ class MainFrameModel {
def results = new ConcurrentHashMap<>()
def downloads = []
def uploads = []
def shared = []
def watched = []
boolean treeVisible = true
def shared
def sharedTree
def treeRoot
final Map<SharedFile, TreeNode> fileToNode = new HashMap<>()
def connectionList = []
def searches = new LinkedList()
def trusted = []
@@ -122,6 +134,10 @@ class MainFrameModel {
void mvcGroupInit(Map<String, Object> args) {
uiSettings = application.context.get("ui-settings")
shared = []
treeRoot = new DefaultMutableTreeNode()
sharedTree = new DefaultTreeModel(treeRoot)
Timer timer = new Timer("download-pumper", true)
timer.schedule({
@@ -165,7 +181,6 @@ class MainFrameModel {
core = e.getNewValue()
routerPresent = core.router != null
me = core.me.getHumanReadableName()
core.eventBus.register(UIResultEvent.class, this)
core.eventBus.register(UIResultBatchEvent.class, this)
core.eventBus.register(DownloadStartedEvent.class, this)
core.eventBus.register(ConnectionEvent.class, this)
@@ -233,9 +248,7 @@ class MainFrameModel {
void onAllFilesLoadedEvent(AllFilesLoadedEvent e) {
runInsideUIAsync {
watched.addAll(core.muOptions.watchedDirectories)
builder.getVariable("watched-directories-table").model.fireTableDataChanged()
watched.each { core.eventBus.publish(new FileSharedEvent(file : new File(it))) }
core.muOptions.watchedDirectories.each { core.eventBus.publish(new FileSharedEvent(file : new File(it))) }
core.muOptions.trustSubscriptions.each {
core.eventBus.publish(new TrustSubscriptionEvent(persona : it, subscribe : true))
@@ -303,7 +316,6 @@ class MainFrameModel {
void onFileHashingEvent(FileHashingEvent e) {
runInsideUIAsync {
loadedFiles = shared.size()
hashingFile = e.hashingFile
}
}
@@ -319,6 +331,8 @@ class MainFrameModel {
loadedFiles = shared.size()
JTable table = builder.getVariable("shared-files-table")
table.model.fireTableDataChanged()
insertIntoTree(e.sharedFile)
loadedFiles = fileToNode.size()
}
}
@@ -328,6 +342,8 @@ class MainFrameModel {
loadedFiles = shared.size()
JTable table = builder.getVariable("shared-files-table")
table.model.fireTableDataChanged()
insertIntoTree(e.loadedFile)
loadedFiles = fileToNode.size()
}
}
@@ -335,8 +351,26 @@ class MainFrameModel {
runInsideUIAsync {
shared.remove(e.unsharedFile)
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 +513,44 @@ class MainFrameModel {
shared << e.downloadedFile
JTable table = builder.getVariable("shared-files-table")
table.model.fireTableDataChanged()
insertIntoTree(e.downloadedFile)
loadedFiles = fileToNode.size()
}
}
private void insertIntoTree(SharedFile file) {
List<File> parents = new ArrayList<>()
File tmp = file.file.getParentFile()
while(tmp.getParent() != null) {
parents << tmp
tmp = tmp.getParentFile()
}
Collections.reverse(parents)
TreeNode node = treeRoot
for(File path : parents) {
boolean exists = false
def children = node.children()
def child = null
while(children.hasMoreElements()) {
child = children.nextElement()
def userObject = child.getUserObject()
if (userObject != null && userObject.file == path) {
exists = true
break
}
}
if (!exists) {
child = new DefaultMutableTreeNode(new InterimTreeNode(path))
node.add(child)
}
node = child
}
def dmtn = new DefaultMutableTreeNode(file)
fileToNode.put(file, dmtn)
node.add(dmtn)
view.refreshSharedFiles()
}
private static class UIConnection {
Destination destination

View File

@@ -13,7 +13,10 @@ class OptionsModel {
@Observable String updateCheckInterval
@Observable boolean autoDownloadUpdate
@Observable boolean shareDownloadedFiles
@Observable boolean shareHiddenFiles
@Observable String downloadLocation
@Observable boolean searchComments
@Observable boolean browseFiles
// i2p options
@Observable String inboundLength
@@ -27,6 +30,8 @@ class OptionsModel {
@Observable boolean showMonitor
@Observable String lnf
@Observable String font
@Observable boolean automaticFontSize
@Observable int customFontSize
@Observable boolean clearCancelledDownloads
@Observable boolean clearFinishedDownloads
@Observable boolean excludeLocalResult
@@ -49,7 +54,10 @@ class OptionsModel {
updateCheckInterval = settings.updateCheckInterval
autoDownloadUpdate = settings.autoDownloadUpdate
shareDownloadedFiles = settings.shareDownloadedFiles
shareHiddenFiles = settings.shareHiddenFiles
downloadLocation = settings.downloadLocation.getAbsolutePath()
searchComments = settings.searchComments
browseFiles = settings.browseFiles
Core core = application.context.get("core")
inboundLength = core.i2pOptions["inbound.length"]
@@ -63,6 +71,8 @@ class OptionsModel {
showMonitor = uiSettings.showMonitor
lnf = uiSettings.lnf
font = uiSettings.font
automaticFontSize = uiSettings.autoFontSize
customFontSize = uiSettings.fontSize
clearCancelledDownloads = uiSettings.clearCancelledDownloads
clearFinishedDownloads = uiSettings.clearFinishedDownloads
excludeLocalResult = uiSettings.excludeLocalResult

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 598 B

View File

@@ -0,0 +1,200 @@
package com.muwire.gui
import griffon.core.artifact.GriffonView
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import net.i2p.data.Base64
import javax.swing.JDialog
import javax.swing.JLabel
import javax.swing.JMenuItem
import javax.swing.JPopupMenu
import javax.swing.ListSelectionModel
import javax.swing.SwingConstants
import javax.swing.table.DefaultTableCellRenderer
import com.muwire.core.search.UIResultEvent
import java.awt.BorderLayout
import java.awt.Toolkit
import java.awt.datatransfer.StringSelection
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
import java.awt.event.WindowAdapter
import java.awt.event.WindowEvent
import javax.annotation.Nonnull
@ArtifactProviderFor(GriffonView)
class BrowseView {
@MVCMember @Nonnull
FactoryBuilderSupport builder
@MVCMember @Nonnull
BrowseModel model
@MVCMember @Nonnull
BrowseController controller
def mainFrame
def dialog
def p
def resultsTable
def lastSortEvent
void initUI() {
mainFrame = application.windowManager.findWindow("main-frame")
dialog = new JDialog(mainFrame, model.host.getHumanReadableName(), true)
dialog.setResizable(true)
p = builder.panel {
borderLayout()
panel (constraints : BorderLayout.NORTH) {
label(text: "Status:")
label(text: bind {model.status.toString()})
}
scrollPane (constraints : BorderLayout.CENTER){
resultsTable = table(autoCreateRowSorter : true) {
tableModel(list : model.results) {
closureColumn(header: "Name", preferredWidth: 350, type: String, read : {row -> row.name.replace('<','_')})
closureColumn(header: "Size", preferredWidth: 20, type: Long, read : {row -> row.size})
closureColumn(header: "Comments", preferredWidth: 20, type: Boolean, read : {row -> row.comment != null})
}
}
}
panel (constraints : BorderLayout.SOUTH) {
button(text : "Download", enabled : bind {model.downloadActionEnabled}, downloadAction)
button(text : "View Comment", enabled : bind{model.viewCommentActionEnabled}, viewCommentAction)
button(text : "Dismiss", dismissAction)
}
}
def centerRenderer = new DefaultTableCellRenderer()
centerRenderer.setHorizontalAlignment(JLabel.CENTER)
resultsTable.setDefaultRenderer(Integer.class,centerRenderer)
resultsTable.columnModel.getColumn(1).setCellRenderer(new SizeRenderer())
resultsTable.rowSorter.addRowSorterListener({evt -> lastSortEvent = evt})
resultsTable.rowSorter.setSortsOnUpdates(true)
def selectionModel = resultsTable.getSelectionModel()
selectionModel.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION)
selectionModel.addListSelectionListener({
int[] rows = resultsTable.getSelectedRows()
if (rows.length == 0) {
model.downloadActionEnabled = false
model.viewCommentActionEnabled = false
return
}
if (lastSortEvent != null) {
for (int i = 0; i < rows.length; i ++) {
rows[i] = resultsTable.rowSorter.convertRowIndexToModel(rows[i])
}
}
boolean downloadActionEnabled = true
if (rows.length == 1 && model.results[rows[0]].comment != null)
model.viewCommentActionEnabled = true
else
model.viewCommentActionEnabled = false
rows.each {
downloadActionEnabled &= mvcGroup.parentGroup.parentGroup.model.canDownload(model.results[it].infohash)
}
model.downloadActionEnabled = downloadActionEnabled
resultsTable.addMouseListener(new MouseAdapter() {
public void mouseReleased(MouseEvent e) {
if (e.isPopupTrigger())
showMenu(e)
}
public void mousePressed(MouseEvent e) {
if (e.isPopupTrigger())
showMenu(e)
}
})
})
}
private void showMenu(MouseEvent e) {
JPopupMenu menu = new JPopupMenu()
if (model.downloadActionEnabled) {
JMenuItem download = new JMenuItem("Download")
download.addActionListener({controller.download()})
menu.add(download)
}
if (model.viewCommentActionEnabled) {
JMenuItem viewComment = new JMenuItem("View Comment")
viewComment.addActionListener({controller.viewComment()})
menu.add(viewComment)
}
JMenuItem copyHash = new JMenuItem("Copy Hash To Clipboard")
copyHash.addActionListener({
List<UIResultEvent> results = selectedResults()
def hash = ""
for(Iterator<UIResultEvent> iter = results.iterator(); iter.hasNext();) {
UIResultEvent result = iter.next()
hash += Base64.encode(result.infohash.getRoot())
if (iter.hasNext())
hash += "\n"
}
copyString(hash)
})
menu.add(copyHash)
JMenuItem copyName = new JMenuItem("Copy Name To Clipboard")
copyName.addActionListener({
List<UIResultEvent> results = selectedResults()
def name = ""
for(Iterator<UIResultEvent> iter = results.iterator(); iter.hasNext();) {
UIResultEvent result = iter.next()
name += result.getName()
if (iter.hasNext())
name += "\n"
}
copyString(name)
})
menu.add(copyName)
menu.show(e.getComponent(), e.getX(), e.getY())
}
private static copyString(String s) {
StringSelection selection = new StringSelection(s)
def clipboard = Toolkit.getDefaultToolkit().getSystemClipboard()
clipboard.setContents(selection, null)
}
void mvcGroupInit(Map<String,String> args) {
controller.register()
dialog.getContentPane().add(p)
dialog.setSize(700, 400)
dialog.setLocationRelativeTo(mainFrame)
dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE)
dialog.addWindowListener( new WindowAdapter() {
public void windowClosed(WindowEvent e) {
mvcGroup.destroy()
}
})
dialog.show()
}
def selectedResults() {
int [] rows = resultsTable.getSelectedRows()
if (rows.length == 0)
return null
if (lastSortEvent != null) {
for (int i = 0; i < rows.length; i ++) {
rows[i] = resultsTable.rowSorter.convertRowIndexToModel(rows[i])
}
}
List<UIResultEvent> rv = new ArrayList<>()
for (Integer i : rows)
rv << model.results[i]
rv
}
}

View File

@@ -16,11 +16,14 @@ import javax.swing.JMenuItem
import javax.swing.JPopupMenu
import javax.swing.JSplitPane
import javax.swing.JTable
import javax.swing.JTree
import javax.swing.ListSelectionModel
import javax.swing.SwingConstants
import javax.swing.TransferHandler
import javax.swing.border.Border
import javax.swing.table.DefaultTableCellRenderer
import javax.swing.tree.TreeNode
import javax.swing.tree.TreePath
import com.muwire.core.Constants
import com.muwire.core.MuWireSettings
@@ -56,11 +59,12 @@ class MainFrameView {
def downloadsTable
def lastDownloadSortEvent
def lastSharedSortEvent
def lastWatchedSortEvent
def trustTablesSortEvents = [:]
UISettings settings
void initUI() {
UISettings settings = application.context.get("ui-settings")
settings = application.context.get("ui-settings")
builder.with {
application(size : [1024,768], id: 'main-frame',
locationRelativeTo : null,
@@ -193,44 +197,46 @@ class MainFrameView {
})
}
panel (border : etchedBorder(), constraints : BorderLayout.CENTER) {
gridLayout(cols : 2, rows : 1)
panel {
borderLayout()
scrollPane (constraints : BorderLayout.CENTER) {
table(id : "watched-directories-table", autoCreateRowSorter: true) {
tableModel(list : model.watched) {
closureColumn(header: "Watched Directories", type : String, read : { it })
borderLayout()
panel (id : "shared-files-panel", constraints : BorderLayout.CENTER){
cardLayout()
panel (constraints : "shared files table") {
borderLayout()
scrollPane(constraints : BorderLayout.CENTER) {
table(id : "shared-files-table", autoCreateRowSorter: true) {
tableModel(list : model.shared) {
closureColumn(header : "Name", preferredWidth : 500, type : String, read : {row -> row.getCachedPath()})
closureColumn(header : "Size", preferredWidth : 100, type : Long, read : {row -> row.getCachedLength() })
closureColumn(header : "Comments", preferredWidth : 100, type : Boolean, read : {it.getComment() != null})
}
}
}
}
}
panel {
borderLayout()
scrollPane(constraints : BorderLayout.CENTER) {
table(id : "shared-files-table", autoCreateRowSorter: true) {
tableModel(list : model.shared) {
closureColumn(header : "Name", preferredWidth : 500, type : String, read : {row -> row.getCachedPath()})
closureColumn(header : "Size", preferredWidth : 100, type : Long, read : {row -> row.getCachedLength() })
closureColumn(header : "Comments", preferredWidth : 100, type : Boolean, read : {it.getComment() != null})
}
panel (constraints : "shared files tree") {
borderLayout()
scrollPane(constraints : BorderLayout.CENTER) {
def jtree = new JTree(model.sharedTree)
jtree.setCellRenderer(new SharedTreeRenderer())
tree(id : "shared-files-tree", rootVisible : false, expandsSelectedPaths: true, jtree)
}
}
}
}
panel (constraints : BorderLayout.SOUTH) {
gridLayout(rows:1, cols:2)
gridLayout(rows:1, cols:3)
panel {
button(text : "Add directories to watch", actionPerformed : watchDirectories)
button(text : "Share files", actionPerformed : shareFiles)
buttonGroup(id : "sharedViewType")
radioButton(text : "Tree", selected : true, buttonGroup : sharedViewType, actionPerformed : showSharedFilesTree)
radioButton(text : "Table", selected : false, buttonGroup : sharedViewType, actionPerformed : showSharedFilesTable)
}
panel {
button(text : "Share files", actionPerformed : shareFiles)
button(text : "Add Comment", enabled : bind {model.addCommentButtonEnabled}, addCommentAction)
}
panel {
gridLayout(rows : 1, cols : 2)
panel {
label("Shared:")
label(text : bind {model.loadedFiles.toString()})
}
panel {
button(text : "Add Comment", enabled : bind {model.addCommentButtonEnabled}, addCommentAction)
label(text : bind {model.loadedFiles}, id : "shared-files-count")
}
}
}
@@ -411,10 +417,7 @@ class MainFrameView {
public boolean importData(TransferHandler.TransferSupport support) {
def files = support.getTransferable().getTransferData(DataFlavor.javaFileListFlavor)
files.each {
if (it.isDirectory())
watchDirectory(it)
else
model.core.eventBus.publish(new FileSharedEvent(file : it))
model.core.eventBus.publish(new FileSharedEvent(file : it))
}
showUploadsWindow.call()
true
@@ -489,13 +492,7 @@ class MainFrameView {
}
})
// shared files table
def sharedFilesTable = builder.getVariable("shared-files-table")
sharedFilesTable.columnModel.getColumn(1).setCellRenderer(new SizeRenderer())
sharedFilesTable.rowSorter.addRowSorterListener({evt -> lastSharedSortEvent = evt})
sharedFilesTable.rowSorter.setSortsOnUpdates(true)
// shared files menu
JPopupMenu sharedFilesMenu = new JPopupMenu()
JMenuItem copyHashToClipboard = new JMenuItem("Copy hash to clipboard")
copyHashToClipboard.addActionListener({mvcGroup.view.copyHashToClipboard()})
@@ -506,7 +503,8 @@ class MainFrameView {
JMenuItem commentSelectedFiles = new JMenuItem("Comment selected files")
commentSelectedFiles.addActionListener({mvcGroup.controller.addComment()})
sharedFilesMenu.add(commentSelectedFiles)
sharedFilesTable.addMouseListener(new MouseAdapter() {
def sharedFilesMouseListener = new MouseAdapter() {
@Override
public void mouseReleased(MouseEvent e) {
if (e.isPopupTrigger())
@@ -517,7 +515,16 @@ class MainFrameView {
if (e.isPopupTrigger())
showPopupMenu(sharedFilesMenu, e)
}
})
}
// shared files table and tree
def sharedFilesTable = builder.getVariable("shared-files-table")
sharedFilesTable.columnModel.getColumn(1).setCellRenderer(new SizeRenderer())
sharedFilesTable.rowSorter.addRowSorterListener({evt -> lastSharedSortEvent = evt})
sharedFilesTable.rowSorter.setSortsOnUpdates(true)
sharedFilesTable.addMouseListener(sharedFilesMouseListener)
selectionModel = sharedFilesTable.getSelectionModel()
selectionModel.addListSelectionListener({
@@ -526,6 +533,14 @@ class MainFrameView {
return
model.addCommentButtonEnabled = true
})
def sharedFilesTree = builder.getVariable("shared-files-tree")
sharedFilesTree.addMouseListener(sharedFilesMouseListener)
sharedFilesTree.addTreeSelectionListener({
def selectedNode = sharedFilesTree.getLastSelectedPathComponent()
model.addCommentButtonEnabled = selectedNode != null
})
// searches table
def searchesTable = builder.getVariable("searches-table")
@@ -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
def subscriptionTable = builder.getVariable("subscription-table")
subscriptionTable.setDefaultRenderer(Integer.class, centerRenderer)
@@ -649,6 +643,9 @@ class MainFrameView {
model.markNeutralFromDistrustedButtonEnabled = true
}
})
// show tree by default
showSharedFilesTree.call()
}
private static void showPopupMenu(JPopupMenu menu, MouseEvent event) {
@@ -656,22 +653,42 @@ class MainFrameView {
}
def selectedSharedFiles() {
def sharedFilesTable = builder.getVariable("shared-files-table")
int[] selected = sharedFilesTable.getSelectedRows()
if (selected.length == 0)
return null
List<SharedFile> rv = new ArrayList<>()
if (lastSharedSortEvent != null) {
for (int i = 0; i < selected.length; i ++) {
selected[i] = sharedFilesTable.rowSorter.convertRowIndexToModel(selected[i])
if (!model.treeVisible) {
def sharedFilesTable = builder.getVariable("shared-files-table")
int[] selected = sharedFilesTable.getSelectedRows()
if (selected.length == 0)
return null
List<SharedFile> rv = new ArrayList<>()
if (lastSharedSortEvent != null) {
for (int i = 0; i < selected.length; i ++) {
selected[i] = sharedFilesTable.rowSorter.convertRowIndexToModel(selected[i])
}
}
selected.each {
rv.add(model.shared[it])
}
return rv
} else {
def sharedFilesTree = builder.getVariable("shared-files-tree")
List<SharedFile> rv = new ArrayList<>()
for (TreePath path : sharedFilesTree.getSelectionPaths()) {
getLeafs(path.getLastPathComponent(), rv)
}
return rv
}
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 selectedFiles = selectedSharedFiles()
if (selectedFiles == null)
@@ -820,53 +837,34 @@ class MainFrameView {
model.monitorPaneButtonEnabled = true
model.trustPaneButtonEnabled = false
}
def showSharedFilesTable = {
model.treeVisible = false
def cardsPanel = builder.getVariable("shared-files-panel")
cardsPanel.getLayout().show(cardsPanel, "shared files table")
}
def showSharedFilesTree = {
model.treeVisible = true
def cardsPanel = builder.getVariable("shared-files-panel")
cardsPanel.getLayout().show(cardsPanel, "shared files tree")
}
def shareFiles = {
def chooser = new JFileChooser()
chooser.setFileHidingEnabled(false)
chooser.setFileHidingEnabled(!model.core.muOptions.shareHiddenFiles)
chooser.setDialogTitle("Select file to share")
chooser.setFileSelectionMode(JFileChooser.FILES_ONLY)
chooser.setMultiSelectionEnabled(true)
int rv = chooser.showOpenDialog(null)
if (rv == JFileChooser.APPROVE_OPTION) {
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) {
def table = builder.getVariable(tableName)
int selectedRow = table.getSelectedRow()
@@ -876,4 +874,12 @@ class MainFrameView {
selectedRow = table.rowSorter.convertRowIndexToModel(selectedRow)
selectedRow
}
public void refreshSharedFiles() {
def tree = builder.getVariable("shared-files-tree")
TreePath[] selectedPaths = tree.getSelectionPaths()
model.sharedTree.nodeStructureChanged(model.treeRoot)
tree.setSelectionPaths(selectedPaths)
builder.getVariable("shared-files-table").model.fireTableDataChanged()
}
}

View File

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

View File

@@ -63,6 +63,7 @@ class SearchTabView {
tableModel(list : model.senders) {
closureColumn(header : "Sender", preferredWidth : 500, type: String, read : {row -> row.getHumanReadableName()})
closureColumn(header : "Results", preferredWidth : 20, type: Integer, read : {row -> model.sendersBucket[row].size()})
closureColumn(header : "Browse", preferredWidth : 20, type: Boolean, read : {row -> model.sendersBucket[row].first().browse})
closureColumn(header : "Trust", preferredWidth : 50, type: String, read : { row ->
model.core.trustService.getLevel(row.destination).toString()
})
@@ -70,9 +71,15 @@ class SearchTabView {
}
}
panel(constraints : BorderLayout.SOUTH) {
button(text : "Trust", enabled: bind {model.trustButtonsEnabled }, trustAction)
button(text : "Neutral", enabled: bind {model.trustButtonsEnabled}, neutralAction)
button(text : "Distrust", enabled : bind {model.trustButtonsEnabled}, distrustAction)
gridLayout(rows: 1, cols : 2)
panel (border : etchedBorder()){
button(text : "Browse Host", enabled : bind {model.browseActionEnabled}, browseAction)
}
panel (border : etchedBorder()){
button(text : "Trust", enabled: bind {model.trustButtonsEnabled }, trustAction)
button(text : "Neutral", enabled: bind {model.trustButtonsEnabled}, neutralAction)
button(text : "Distrust", enabled : bind {model.trustButtonsEnabled}, distrustAction)
}
}
}
panel {
@@ -93,6 +100,7 @@ class SearchTabView {
panel()
panel {
button(text : "Download", enabled : bind {model.downloadActionEnabled}, downloadAction)
button(text : "View Comment", enabled : bind {model.viewCommentActionEnabled}, showCommentAction)
}
panel {
gridBagLayout()
@@ -183,6 +191,16 @@ class SearchTabView {
}
})
resultsTable.getSelectionModel().addListSelectionListener({
def result = getSelectedResult()
if (result == null) {
model.viewCommentActionEnabled = false
return
} else {
model.viewCommentActionEnabled = result.comment != null
}
})
// senders table
sendersTable.setDefaultRenderer(Integer.class, centerRenderer)
sendersTable.rowSorter.addRowSorterListener({evt -> lastSendersSortEvent = evt})
@@ -193,12 +211,14 @@ class SearchTabView {
int row = selectedSenderRow()
if (row < 0) {
model.trustButtonsEnabled = false
model.browseActionEnabled = false
return
} else {
Persona sender = model.senders[row]
model.browseActionEnabled = model.sendersBucket[sender].first().browse
model.trustButtonsEnabled = true
model.results.clear()
Persona p = model.senders[row]
model.results.addAll(model.sendersBucket[p])
model.results.addAll(model.sendersBucket[sender])
resultsTable.model.fireTableDataChanged()
}
})
@@ -226,50 +246,49 @@ class SearchTabView {
JMenuItem copyHashToClipboard = new JMenuItem("Copy hash to clipboard")
copyHashToClipboard.addActionListener({mvcGroup.view.copyHashToClipboard()})
menu.add(copyHashToClipboard)
JMenuItem copyNameToClipboard = new JMenuItem("Copy name to clipboard")
copyNameToClipboard.addActionListener({mvcGroup.view.copyNameToClipboard()})
menu.add(copyNameToClipboard)
showMenu = true
// show comment if any
int selectedRow = resultsTable.getSelectedRow()
if (lastSortEvent != null)
selectedRow = resultsTable.rowSorter.convertRowIndexToModel(selectedRow)
if (model.results[selectedRow].comment != null) {
JMenuItem showComment = new JMenuItem("Show Comment")
showComment.addActionListener({mvcGroup.view.showComment()})
if (model.viewCommentActionEnabled) {
JMenuItem showComment = new JMenuItem("View Comment")
showComment.addActionListener({mvcGroup.controller.showComment()})
menu.add(showComment)
}
}
if (showMenu)
menu.show(e.getComponent(), e.getX(), e.getY())
}
def copyHashToClipboard() {
private UIResultEvent getSelectedResult() {
int[] selectedRows = resultsTable.getSelectedRows()
if (selectedRows.length != 1)
return
return null
int selected = selectedRows[0]
if (lastSortEvent != null)
selected = resultsTable.rowSorter.convertRowIndexToModel(selected)
String hash = Base64.encode(model.results[selected].infohash.getRoot())
model.results[selected]
}
def copyHashToClipboard() {
def result = getSelectedResult()
if (result == null)
return
String hash = Base64.encode(result.infohash.getRoot())
StringSelection selection = new StringSelection(hash)
def clipboard = Toolkit.getDefaultToolkit().getSystemClipboard()
clipboard.setContents(selection, null)
}
def showComment() {
int selectedRow = resultsTable.getSelectedRow()
if (selectedRow < 0)
def copyNameToClipboard() {
def result = getSelectedResult()
if (result == null)
return
if (lastSortEvent != null)
selectedRow = resultsTable.rowSorter.convertRowIndexToModel(selectedRow)
UIResultEvent event = model.results[selectedRow]
if (event.comment == null)
return
String groupId = Base64.encode(event.infohash.getRoot())
Map<String,Object> params = new HashMap<>()
params['result'] = event
mvcGroup.createMVCGroup("show-comment", groupId, params)
StringSelection selection = new StringSelection(result.getName())
def clipboard = Toolkit.getDefaultToolkit().getSystemClipboard()
clipboard.setContents(selection, null)
}
int selectedSenderRow() {

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
boolean showMonitor
String font
boolean autoFontSize
int fontSize
boolean clearCancelledDownloads
boolean clearFinishedDownloads
boolean excludeLocalResult
boolean showSearchHashes
UISettings(Properties props) {
lnf = props.getProperty("lnf", "system")
showMonitor = Boolean.parseBoolean(props.getProperty("showMonitor", "false"))
@@ -18,6 +20,8 @@ class UISettings {
clearFinishedDownloads = Boolean.parseBoolean(props.getProperty("clearFinishedDownloads","false"))
excludeLocalResult = Boolean.parseBoolean(props.getProperty("excludeLocalResult","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 {
@@ -28,6 +32,8 @@ class UISettings {
props.setProperty("clearFinishedDownloads", String.valueOf(clearFinishedDownloads))
props.setProperty("excludeLocalResult", String.valueOf(excludeLocalResult))
props.setProperty("showSearchHashes", String.valueOf(showSearchHashes))
props.setProperty("autoFontSize", String.valueOf(autoFontSize))
props.setProperty("fontSize", String.valueOf(fontSize))
if (font != null)
props.setProperty("font", font)